Skip to content
Draft
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
88 changes: 88 additions & 0 deletions SPECS/expat/CVE-2026-32776.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
From 90376a0668e575098017898c4ab54d6d5e3d0451 Mon Sep 17 00:00:00 2001
From: Francesco Bertolaccini <francesco.bertolaccini@trailofbits.com>
Date: Tue, 3 Mar 2026 16:41:43 +0100
Subject: [PATCH] Fix NULL function-pointer dereference for empty external
parameter entities

When an external parameter entity with empty text is referenced inside
an entity declaration value, the sub-parser created to handle it receives
0 bytes of input. Processing enters entityValueInitProcessor which calls
storeEntityValue() with the parser's encoding; since no bytes were ever
processed, encoding detection has not yet occurred and the encoding is
still the initial probing encoding set up by XmlInitEncoding(). That
encoding only populates scanners[] (for prolog and content), not
literalScanners[]. XmlEntityValueTok() calls through
literalScanners[XML_ENTITY_VALUE_LITERAL] which is NULL, causing a
SEGV.

Skip the tokenization loop entirely when entityTextPtr >= entityTextEnd,
and initialize the `next` pointer before the early exit so that callers
(callStoreEntityValue) receive a valid value through nextPtr.

Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: https://github.com/libexpat/libexpat/pull/1158.patch
---
lib/xmlparse.c | 9 ++++++++-
tests/basic_tests.c | 19 +++++++++++++++++++
2 files changed, 27 insertions(+), 1 deletion(-)

diff --git a/lib/xmlparse.c b/lib/xmlparse.c
index 0bf913c..d126f5b 100644
--- a/lib/xmlparse.c
+++ b/lib/xmlparse.c
@@ -6765,7 +6765,14 @@ storeEntityValue(XML_Parser parser, const ENCODING *enc,
return XML_ERROR_NO_MEMORY;
}

- const char *next;
+ const char *next = entityTextPtr;
+
+ /* Nothing to tokenize. */
+ if (entityTextPtr >= entityTextEnd) {
+ result = XML_ERROR_NONE;
+ goto endEntityValue;
+ }
+
for (;;) {
next
= entityTextPtr; /* XmlEntityValueTok doesn't always set the last arg */
diff --git a/tests/basic_tests.c b/tests/basic_tests.c
index 2db2a76..cfb09f3 100644
--- a/tests/basic_tests.c
+++ b/tests/basic_tests.c
@@ -6123,6 +6123,24 @@ START_TEST(test_varying_buffer_fills) {
}
END_TEST

+START_TEST(test_empty_ext_param_entity_in_value) {
+ const char *text = "<!DOCTYPE r SYSTEM \"ext.dtd\"><r/>";
+ ExtOption options[] = {
+ {XCS("ext.dtd"), "<!ENTITY % pe SYSTEM \"empty\">"
+ "<!ENTITY ge \"%pe;\">"},
+ {XCS("empty"), ""},
+ {NULL, NULL},
+ };
+
+ XML_SetParamEntityParsing(g_parser, XML_PARAM_ENTITY_PARSING_ALWAYS);
+ XML_SetExternalEntityRefHandler(g_parser, external_entity_optioner);
+ XML_SetUserData(g_parser, options);
+ if (_XML_Parse_SINGLE_BYTES(g_parser, text, (int)strlen(text), XML_TRUE)
+ == XML_STATUS_ERROR)
+ xml_failure(g_parser);
+}
+END_TEST
+
void
make_basic_test_case(Suite *s) {
TCase *tc_basic = tcase_create("basic tests");
@@ -6368,6 +6386,7 @@ make_basic_test_case(Suite *s) {
tcase_add_test(tc_basic, test_empty_element_abort);
tcase_add_test__ifdef_xml_dtd(tc_basic,
test_pool_integrity_with_unfinished_attr);
+ tcase_add_test__ifdef_xml_dtd(tc_basic, test_empty_ext_param_entity_in_value);
tcase_add_test__if_xml_ge(tc_basic, test_entity_ref_no_elements);
tcase_add_test__if_xml_ge(tc_basic, test_deep_nested_entity);
tcase_add_test__if_xml_ge(tc_basic, test_deep_nested_attribute_entity);
--
2.45.4

54 changes: 54 additions & 0 deletions SPECS/expat/CVE-2026-32777.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
From 559a9353eac1e96e56a03abe18efe0bc8ed7dddf Mon Sep 17 00:00:00 2001
From: Sebastian Pipping <sebastian@pipping.org>
Date: Sun, 1 Mar 2026 20:16:13 +0100
Subject: [PATCH 1/2] lib: Reject XML_TOK_INSTANCE_START infinite loop in
entityValueProcessor

.. that OSS-Fuzz/ClusterFuzz uncovered
---
lib/xmlparse.c | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/lib/xmlparse.c b/lib/xmlparse.c
index d126f5b..88361a7 100644
--- a/lib/xmlparse.c
+++ b/lib/xmlparse.c
@@ -5068,7 +5068,7 @@ entityValueInitProcessor(XML_Parser parser, const char *s, const char *end,
}
/* If we get this token, we have the start of what might be a
normal tag, but not a declaration (i.e. it doesn't begin with
- "<!"). In a DTD context, that isn't legal.
+ "<!" or "<?"). In a DTD context, that isn't legal.
*/
else if (tok == XML_TOK_INSTANCE_START) {
*nextPtr = next;
@@ -5157,6 +5157,15 @@ entityValueProcessor(XML_Parser parser, const char *s, const char *end,
/* found end of entity value - can store it now */
return storeEntityValue(parser, enc, s, end, XML_ACCOUNT_DIRECT, NULL);
}
+ /* If we get this token, we have the start of what might be a
+ normal tag, but not a declaration (i.e. it doesn't begin with
+ "<!" or "<?"). In a DTD context, that isn't legal.
+ */
+ else if (tok == XML_TOK_INSTANCE_START) {
+ *nextPtr = next;
+ return XML_ERROR_SYNTAX;
+ }
+
start = next;
}
}
--
2.45.4


From 88e7ccdf240713ead3082d79b8bec91161bdab2f Mon Sep 17 00:00:00 2001
From: Sebastian Pipping <sebastian@pipping.org>
Date: Fri, 6 Mar 2026 18:31:34 +0100
Subject: [PATCH 2/2] misc_tests.c: Cover XML_TOK_INSTANCE_START infinite loop
case

.. that OSS-Fuzz/ClusterFuzz uncovered
--
2.45.4

145 changes: 145 additions & 0 deletions SPECS/expat/CVE-2026-32778.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
From 643882f03c3e3db0ec9e0d851322a9e087028622 Mon Sep 17 00:00:00 2001
From: laserbear <10689391+Laserbear@users.noreply.github.com>
Date: Sun, 8 Mar 2026 17:28:06 -0700
Subject: [PATCH 1/2] copy prefix name to pool before lookup

.. so that we cannot end up with a zombie PREFIX in the pool
that has NULL for a name.

Co-authored-by: Sebastian Pipping <sebastian@pipping.org>
---
lib/xmlparse.c | 43 +++++++++++++++++++++++++++++++++++--------
1 file changed, 35 insertions(+), 8 deletions(-)

diff --git a/lib/xmlparse.c b/lib/xmlparse.c
index 88361a7..69027f6 100644
--- a/lib/xmlparse.c
+++ b/lib/xmlparse.c
@@ -590,6 +590,8 @@ static XML_Char *poolStoreString(STRING_POOL *pool, const ENCODING *enc,
static XML_Bool FASTCALL poolGrow(STRING_POOL *pool);
static const XML_Char *FASTCALL poolCopyString(STRING_POOL *pool,
const XML_Char *s);
+static const XML_Char *FASTCALL poolCopyStringNoFinish(STRING_POOL *pool,
+ const XML_Char *s);
static const XML_Char *poolCopyStringN(STRING_POOL *pool, const XML_Char *s,
int n);
static const XML_Char *FASTCALL poolAppendString(STRING_POOL *pool,
@@ -7431,16 +7433,24 @@ setContext(XML_Parser parser, const XML_Char *context) {
else {
if (! poolAppendChar(&parser->m_tempPool, XML_T('\0')))
return XML_FALSE;
- prefix
- = (PREFIX *)lookup(parser, &dtd->prefixes,
- poolStart(&parser->m_tempPool), sizeof(PREFIX));
- if (! prefix)
+ const XML_Char *const prefixName = poolCopyStringNoFinish(
+ &dtd->pool, poolStart(&parser->m_tempPool));
+ if (! prefixName) {
return XML_FALSE;
- if (prefix->name == poolStart(&parser->m_tempPool)) {
- prefix->name = poolCopyString(&dtd->pool, prefix->name);
- if (! prefix->name)
- return XML_FALSE;
}
+
+ prefix = (PREFIX *)lookup(parser, &dtd->prefixes, prefixName,
+ sizeof(PREFIX));
+
+ const bool prefixNameUsed = prefix && prefix->name == prefixName;
+ if (prefixNameUsed)
+ poolFinish(&dtd->pool);
+ else
+ poolDiscard(&dtd->pool);
+
+ if (! prefix)
+ return XML_FALSE;
+
poolDiscard(&parser->m_tempPool);
}
for (context = s + 1; *context != CONTEXT_SEP && *context != XML_T('\0');
@@ -8029,6 +8039,23 @@ poolCopyString(STRING_POOL *pool, const XML_Char *s) {
return s;
}

+// A version of `poolCopyString` that does not call `poolFinish`
+// and reverts any partial advancement upon failure.
+static const XML_Char *FASTCALL
+poolCopyStringNoFinish(STRING_POOL *pool, const XML_Char *s) {
+ const XML_Char *const original = s;
+ do {
+ if (! poolAppendChar(pool, *s)) {
+ // Revert any previously successful advancement
+ const ptrdiff_t advancedBy = s - original;
+ if (advancedBy > 0)
+ pool->ptr -= advancedBy;
+ return NULL;
+ }
+ } while (*s++);
+ return pool->start;
+}
+
static const XML_Char *
poolCopyStringN(STRING_POOL *pool, const XML_Char *s, int n) {
if (! pool->ptr && ! poolGrow(pool)) {
--
2.45.4


From 8d541f0de1c513ab1030a2694041065d325f9aca Mon Sep 17 00:00:00 2001
From: laserbear <10689391+Laserbear@users.noreply.github.com>
Date: Sun, 8 Mar 2026 17:28:06 -0700
Subject: [PATCH 2/2] test that we do not end up with a zombie PREFIX in the
pool

Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: https://github.com/libexpat/libexpat/pull/1163.patch
---
tests/nsalloc_tests.c | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)

diff --git a/tests/nsalloc_tests.c b/tests/nsalloc_tests.c
index a8f5718..d284a58 100644
--- a/tests/nsalloc_tests.c
+++ b/tests/nsalloc_tests.c
@@ -1505,6 +1505,32 @@ START_TEST(test_nsalloc_prefixed_element) {
}
END_TEST

+/* Verify that retry after OOM in setContext() does not crash.
+ */
+START_TEST(test_nsalloc_setContext_zombie) {
+ const char *text = "<doc>Hello</doc>";
+ unsigned int i;
+ const unsigned int max_alloc_count = 30;
+
+ for (i = 0; i < max_alloc_count; i++) {
+ g_allocation_count = (int)i;
+ if (XML_Parse(g_parser, text, (int)strlen(text), XML_TRUE)
+ != XML_STATUS_ERROR)
+ break;
+ /* Retry on the same parser — must not crash */
+ g_allocation_count = ALLOC_ALWAYS_SUCCEED;
+ XML_Parse(g_parser, text, (int)strlen(text), XML_TRUE);
+
+ nsalloc_teardown();
+ nsalloc_setup();
+ }
+ if (i == 0)
+ fail("Parsing worked despite failing allocations");
+ else if (i == max_alloc_count)
+ fail("Parsing failed even at maximum allocation count");
+}
+END_TEST
+
void
make_nsalloc_test_case(Suite *s) {
TCase *tc_nsalloc = tcase_create("namespace allocation tests");
@@ -1539,4 +1565,5 @@ make_nsalloc_test_case(Suite *s) {
tcase_add_test__if_xml_ge(tc_nsalloc, test_nsalloc_long_default_in_ext);
tcase_add_test(tc_nsalloc, test_nsalloc_long_systemid_in_ext);
tcase_add_test(tc_nsalloc, test_nsalloc_prefixed_element);
+ tcase_add_test(tc_nsalloc, test_nsalloc_setContext_zombie);
}
--
2.45.4

8 changes: 7 additions & 1 deletion SPECS/expat/expat.spec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Summary: An XML parser library
Name: expat
Version: 2.6.4
Release: 4%{?dist}
Release: 5%{?dist}
License: MIT
Vendor: Microsoft Corporation
Distribution: Mariner
Expand All @@ -13,6 +13,9 @@ Patch0: CVE-2024-8176.patch
Patch1: CVE-2025-59375.patch
Patch2: CVE-2026-24515.patch
Patch3: CVE-2026-25210.patch
Patch4: CVE-2026-32776.patch
Patch5: CVE-2026-32777.patch
Patch6: CVE-2026-32778.patch

Requires: %{name}-libs = %{version}-%{release}

Expand Down Expand Up @@ -71,6 +74,9 @@ rm -rf %{buildroot}/%{_docdir}/%{name}
%{_libdir}/libexpat.so.1*

%changelog
* Wed Mar 18 2026 Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> - 2.6.4-5
- Patch for CVE-2026-32778, CVE-2026-32777, CVE-2026-32776

* Mon Feb 02 2026 Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> - 2.6.4-4
- Patch for CVE-2026-25210

Expand Down
6 changes: 3 additions & 3 deletions toolkit/resources/manifests/package/pkggen_core_aarch64.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ elfutils-libelf-0.186-2.cm2.aarch64.rpm
elfutils-libelf-devel-0.186-2.cm2.aarch64.rpm
elfutils-libelf-devel-static-0.186-2.cm2.aarch64.rpm
elfutils-libelf-lang-0.186-2.cm2.aarch64.rpm
expat-2.6.4-4.cm2.aarch64.rpm
expat-devel-2.6.4-4.cm2.aarch64.rpm
expat-libs-2.6.4-4.cm2.aarch64.rpm
expat-2.6.4-5.cm2.aarch64.rpm
expat-devel-2.6.4-5.cm2.aarch64.rpm
expat-libs-2.6.4-5.cm2.aarch64.rpm
libpipeline-1.5.5-3.cm2.aarch64.rpm
libpipeline-devel-1.5.5-3.cm2.aarch64.rpm
gdbm-1.21-1.cm2.aarch64.rpm
Expand Down
6 changes: 3 additions & 3 deletions toolkit/resources/manifests/package/pkggen_core_x86_64.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ elfutils-libelf-0.186-2.cm2.x86_64.rpm
elfutils-libelf-devel-0.186-2.cm2.x86_64.rpm
elfutils-libelf-devel-static-0.186-2.cm2.x86_64.rpm
elfutils-libelf-lang-0.186-2.cm2.x86_64.rpm
expat-2.6.4-4.cm2.x86_64.rpm
expat-devel-2.6.4-4.cm2.x86_64.rpm
expat-libs-2.6.4-4.cm2.x86_64.rpm
expat-2.6.4-5.cm2.x86_64.rpm
expat-devel-2.6.4-5.cm2.x86_64.rpm
expat-libs-2.6.4-5.cm2.x86_64.rpm
libpipeline-1.5.5-3.cm2.x86_64.rpm
libpipeline-devel-1.5.5-3.cm2.x86_64.rpm
gdbm-1.21-1.cm2.x86_64.rpm
Expand Down
8 changes: 4 additions & 4 deletions toolkit/resources/manifests/package/toolchain_aarch64.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ elfutils-libelf-0.186-2.cm2.aarch64.rpm
elfutils-libelf-devel-0.186-2.cm2.aarch64.rpm
elfutils-libelf-devel-static-0.186-2.cm2.aarch64.rpm
elfutils-libelf-lang-0.186-2.cm2.aarch64.rpm
expat-2.6.4-4.cm2.aarch64.rpm
expat-debuginfo-2.6.4-4.cm2.aarch64.rpm
expat-devel-2.6.4-4.cm2.aarch64.rpm
expat-libs-2.6.4-4.cm2.aarch64.rpm
expat-2.6.4-5.cm2.aarch64.rpm
expat-debuginfo-2.6.4-5.cm2.aarch64.rpm
expat-devel-2.6.4-5.cm2.aarch64.rpm
expat-libs-2.6.4-5.cm2.aarch64.rpm
file-5.40-3.cm2.aarch64.rpm
file-debuginfo-5.40-3.cm2.aarch64.rpm
file-devel-5.40-3.cm2.aarch64.rpm
Expand Down
8 changes: 4 additions & 4 deletions toolkit/resources/manifests/package/toolchain_x86_64.txt
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ elfutils-libelf-0.186-2.cm2.x86_64.rpm
elfutils-libelf-devel-0.186-2.cm2.x86_64.rpm
elfutils-libelf-devel-static-0.186-2.cm2.x86_64.rpm
elfutils-libelf-lang-0.186-2.cm2.x86_64.rpm
expat-2.6.4-4.cm2.x86_64.rpm
expat-debuginfo-2.6.4-4.cm2.x86_64.rpm
expat-devel-2.6.4-4.cm2.x86_64.rpm
expat-libs-2.6.4-4.cm2.x86_64.rpm
expat-2.6.4-5.cm2.x86_64.rpm
expat-debuginfo-2.6.4-5.cm2.x86_64.rpm
expat-devel-2.6.4-5.cm2.x86_64.rpm
expat-libs-2.6.4-5.cm2.x86_64.rpm
file-5.40-3.cm2.x86_64.rpm
file-debuginfo-5.40-3.cm2.x86_64.rpm
file-devel-5.40-3.cm2.x86_64.rpm
Expand Down
Loading