summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilip Paeps <philip@FreeBSD.org>2026-08-31 15:36:27 +0000
committerPhilip Paeps <philip@FreeBSD.org>2026-08-31 15:36:27 +0000
commit698f424711732909a9b349950543e220eb0dfa08 (patch)
tree3a44d992e311df8ff4f92dc5ebdf43754249dc80
parentef5aa86648f5120e7badb14ea8a6cb2a312fc56b (diff)
Vendor import of expat 2.8.4vendor/expat/2.8.4vendor/expat
-rw-r--r--Changes65
-rw-r--r--README.md2
-rw-r--r--configure.ac2
-rw-r--r--doc/reference.html2
-rw-r--r--doc/xmlwf.12
-rw-r--r--doc/xmlwf.xml2
-rwxr-xr-xfix-xmltest-log.sh3
-rw-r--r--lib/expat.h2
-rw-r--r--lib/internal.h1
-rw-r--r--lib/random_getentropy.c2
-rw-r--r--lib/xmlparse.c253
-rw-r--r--lib/xmltok.h4
-rw-r--r--tests/basic_tests.c79
-rw-r--r--tests/misc_tests.c58
14 files changed, 403 insertions, 74 deletions
diff --git a/Changes b/Changes
index ae1b48f3bfd9..af91a67dd5ce 100644
--- a/Changes
+++ b/Changes
@@ -16,6 +16,71 @@
!! Sebastian Pipping -- Berlin, 2026-08-03 !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+Release 2.8.4 Mon August 31 2026
+ Security fixes:
+ #1321 #1331 CVE-2026-66046, CVE-2026-76641 -- Fix quadratic runtime from
+ "attribute isCdata lookups" that allowed denial of service
+ attacks through moderately sized crafted XML input
+ (CWE-407).
+ The vulnerability is closely related to past CVE-2026-45186
+ that was fixed with Expat 2.8.1.
+ Please note that a layer of compression around XML can
+ significantly reduce the minimum attack payload size.
+ Upstream CVSS 3.1 vector:
+ AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (CVSS score: 7.5)
+ (Note the "AV:N" for network/remote.)
+ #1322 CVE-2026-76957 -- Protect custom encoding callbacks from
+ parser re-entry. The vulnerability is closely related to
+ past issues CVE-2026-50219, CVE-2026-56131 and
+ CVE-2026-56412 that were all fixed with Expat 2.8.2.
+ #1326 CVE-2026-76956 -- Fix inverted getentropy() return handling
+ Allows for hash flooding denial of services in
+ configurations where getentropy is configured or detected
+ as the only high quality entropy extractor.
+ Upstream CVSS 3.1 vector:
+ AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H (CVSS score: 5.9)
+ (Note the "AV:N" for network/remote.)
+
+ Other changes:
+ #1332 #1333 CMake: Only add `/source-charset:utf-8` when `/utf-8` is not
+ present
+ #1315 lib: Resolve (currently unreachable) undefined behavior from
+ overshifting a signed int to the left
+ #1325 #1334 lib: Support read-only hash table lookup with keys that are
+ not zero-terminated
+ #1340 lib: Use a C99 bool for `ENTITY.open`
+ #1319 Fix typo in comment
+ #1320 Sync file headers
+ #1328 #1329 Version info bumped from 13:3:12 (libexpat*.so.1.12.3)
+ to 13:4:12 (libexpat*.so.1.12.4); see https://verbump.de/
+ for what these numbers do
+
+ Infrastructure:
+ #1317 #1335 CI: Cover compilation and execution with Fil-C
+ #1337 CI: Cover compilation and execution on riscv64
+ #1338 CI: Cover compilation and execution with Clang-based MinGW
+ #1339 CI: Cover compilation and execution on (big-endian) s390x
+ #1316 CI: Run test suite with musl, also
+ #1336 CI: Bump WASI SDK from 33 to 34
+ #1345 CI: Bump Clang from 22 to 23
+
+ Special thanks to:
+ Alberto Maschietto
+ Alexander Bluhm
+ Berkay Eren Ürün
+ Darren Carreras
+ Fabian Wahle (Hap Security)
+ Matteo Forzan
+ Matthew Fernandez
+ Sorrashut Kaewtaworn
+ Wade Sparks III
+ Zeyou Liu
+ and
+ City of Munich Open Source Sabbatical
+ Moonshot AI
+ VulnCheck
+ Z.ai
+
Release 2.8.3 Mon August 10 2026
Security fixes:
#1296 CVE-2026-72522 -- Fix an out-of-bounds read and the resulting
diff --git a/README.md b/README.md
index 3e75af77e500..ae43ed3cc663 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@
> Thank you! :heart: :pray:
-# Expat, Release 2.8.3
+# Expat, Release 2.8.4
This is Expat, a C99 library for parsing
[XML 1.0 Fourth Edition](https://www.w3.org/TR/2006/REC-xml-20060816/), started by
diff --git a/configure.ac b/configure.ac
index ce33dd5698c8..be6b8b289197 100644
--- a/configure.ac
+++ b/configure.ac
@@ -90,7 +90,7 @@ dnl If the API changes incompatibly set LIBAGE back to 0
dnl
LIBCURRENT=13 # sync
-LIBREVISION=3 # with
+LIBREVISION=4 # with
LIBAGE=12 # CMakeLists.txt!
AC_CONFIG_HEADERS([expat_config.h])
diff --git a/doc/reference.html b/doc/reference.html
index 022a2a0c043c..e54c7be2965a 100644
--- a/doc/reference.html
+++ b/doc/reference.html
@@ -53,7 +53,7 @@
<body>
<div>
<h1>
- The Expat XML Parser <small>Release 2.8.3</small>
+ The Expat XML Parser <small>Release 2.8.4</small>
</h1>
</div>
diff --git a/doc/xmlwf.1 b/doc/xmlwf.1
index 639760bc6be4..7177cbf19c49 100644
--- a/doc/xmlwf.1
+++ b/doc/xmlwf.1
@@ -5,7 +5,7 @@
\\$2 \(la\\$1\(ra\\$3
..
.if \n(.g .mso www.tmac
-.TH XMLWF 1 "August 10, 2026" "" ""
+.TH XMLWF 1 "August 31, 2026" "" ""
.SH NAME
xmlwf \- Determines if an XML document is well-formed
.SH SYNOPSIS
diff --git a/doc/xmlwf.xml b/doc/xmlwf.xml
index b1b23bee1f6f..93a67c407563 100644
--- a/doc/xmlwf.xml
+++ b/doc/xmlwf.xml
@@ -21,7 +21,7 @@
"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd" [
<!ENTITY dhfirstname "<firstname>Scott</firstname>">
<!ENTITY dhsurname "<surname>Bronson</surname>">
- <!ENTITY dhdate "<date>August 10, 2026</date>">
+ <!ENTITY dhdate "<date>August 31, 2026</date>">
<!-- Please adjust this^^ date whenever cutting a new release. -->
<!ENTITY dhsection "<manvolnum>1</manvolnum>">
<!ENTITY dhemail "<email>bronson@rinspin.com</email>">
diff --git a/fix-xmltest-log.sh b/fix-xmltest-log.sh
index 4deafe53a7a8..e4dbd54275c7 100755
--- a/fix-xmltest-log.sh
+++ b/fix-xmltest-log.sh
@@ -38,6 +38,9 @@ exec "${sed}" -i.bak \
-e '# convert DOS line endings to Unix without resorting to dos2unix' \
-e $'s/\r//' \
\
+ -e '# Filter out "unhandled instruction" lines from AddressSanitizer' \
+ -e '/^==[0-9]\+==interception_win: unhandled instruction at /d' \
+ \
-e 's/^wine: Call .* msvcrt\.dll\._wperror, aborting$/ibm49i02.dtd: No such file or directory/' \
\
-e '/^wine: /d' \
diff --git a/lib/expat.h b/lib/expat.h
index dbebd985a652..b296be9dbad2 100644
--- a/lib/expat.h
+++ b/lib/expat.h
@@ -1096,7 +1096,7 @@ XML_SetReparseDeferralEnabled(XML_Parser parser, XML_Bool enabled);
*/
# define XML_MAJOR_VERSION 2
# define XML_MINOR_VERSION 8
-# define XML_MICRO_VERSION 3
+# define XML_MICRO_VERSION 4
# ifdef __cplusplus
}
diff --git a/lib/internal.h b/lib/internal.h
index 7e67d2e378c5..6311028e94b8 100644
--- a/lib/internal.h
+++ b/lib/internal.h
@@ -33,6 +33,7 @@
Copyright (c) 2019 David Loffredo <loffredo@steptools.com>
Copyright (c) 2023-2024 Sony Corporation / Snild Dolkow <snild@sony.com>
Copyright (c) 2024 Taichi Haradaguchi <20001722@ymail.ne.jp>
+ Copyright (c) 2026 Matthew Wozniczka <mattheww@simba.com>
Licensed under the MIT license:
Permission is hereby granted, free of charge, to any person obtaining
diff --git a/lib/random_getentropy.c b/lib/random_getentropy.c
index d258df6a7678..ad8b1984fea0 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);
diff --git a/lib/xmlparse.c b/lib/xmlparse.c
index ac79b9ce7a3b..2e1936fb84c4 100644
--- a/lib/xmlparse.c
+++ b/lib/xmlparse.c
@@ -1,4 +1,4 @@
-/* ee5f82c3ffd57c5224394ba46f348dbce466d34d6c925a527ae46b1cfe6adf1d (2.8.3+)
+/* 13c4e8da8fccffb0e8e599684e0d447ad14c1bb0b48792cf5dd77d8712301871 (2.8.4+)
__ __ _
___\ \/ /_ __ __ _| |_
/ _ \\ /| '_ \ / _` | __|
@@ -51,6 +51,9 @@
Copyright (c) 2026 Kartik Kenchi <netliomax25@gmail.com>
Copyright (c) 2026 Haris Hussain <hextheshadow0x@gmail.com>
Copyright (c) 2026 Evgeny Kotkov <kotkov@apache.org>
+ Copyright (c) 2026 Darren Carreras <carrerasdarren@gmail.com>
+ Copyright (c) 2026 Alberto Maschietto <albertomaschietto9@gmail.com>
+ Copyright (c) 2026 Zeyou Liu <zeyouliu@tencent.com>
Licensed under the MIT license:
Permission is hereby granted, free of charge, to any person obtaining
@@ -330,7 +333,7 @@ typedef struct {
const XML_Char *base;
const XML_Char *publicId;
const XML_Char *notation;
- XML_Bool open;
+ bool open;
XML_Bool hasMore; /* true if entity has not been completely processed */
/* An entity can be open while being already completely processed (hasMore ==
XML_FALSE). The reason is the delayed closing of entities until their inner
@@ -381,6 +384,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;
@@ -394,7 +413,7 @@ typedef struct {
size_t nDefaultAtts;
size_t allocDefaultAtts;
DEFAULT_ATTRIBUTE *defaultAtts;
- HASH_TABLE defaultAttsNames;
+ HASH_TABLE defaultAttForName;
} ELEMENT_TYPE;
typedef struct {
@@ -579,6 +598,8 @@ static int dtdCopy(XML_Parser oldParser, DTD *newDtd, const DTD *oldDtd,
XML_Parser parser);
static int copyEntityTable(XML_Parser oldParser, HASH_TABLE *newTable,
STRING_POOL *newPool, const HASH_TABLE *oldTable);
+static NAMED *lookupWithLength(XML_Parser parser, HASH_TABLE *table, KEY name,
+ size_t nameLen, size_t createSize);
static NAMED *lookup(XML_Parser parser, HASH_TABLE *table, KEY name,
size_t createSize);
static void FASTCALL hashTableInit(HASH_TABLE *table, XML_Parser parser);
@@ -755,6 +776,8 @@ struct XML_ParserStruct {
void *m_unknownEncodingMem;
void *m_unknownEncodingData;
void *m_unknownEncodingHandlerData;
+ // Application callback invoked by callUnknownEncodingConvert.
+ int(XMLCALL *m_unknownEncodingConvert)(void *, const char *);
void(XMLCALL *m_unknownEncodingRelease)(void *);
PROLOG_STATE m_prologState;
Processor *m_processor;
@@ -1177,6 +1200,25 @@ isCalledFromInsideHandler(XML_Parser parser) {
return parser->m_handlerCallDepth > 0;
}
+static void
+callUnknownEncodingRelease(XML_Parser parser) {
+ beforeHandler(parser);
+ parser->m_unknownEncodingRelease(parser->m_unknownEncodingData);
+ afterHandler(parser);
+ parser->m_unknownEncodingRelease = NULL;
+ parser->m_unknownEncodingData = NULL;
+}
+
+static int XMLCALL
+callUnknownEncodingConvert(void *data, const char *p) {
+ XML_Parser parser = data;
+ beforeHandler(parser);
+ const int result
+ = parser->m_unknownEncodingConvert(parser->m_unknownEncodingData, p);
+ afterHandler(parser);
+ return result;
+}
+
static enum XML_Error
callProcessor(XML_Parser parser, const char *start, const char *end,
const char **endPtr) {
@@ -1524,6 +1566,7 @@ parserInit(XML_Parser parser, const XML_Char *encodingName) {
parser->m_inheritedBindings = NULL;
parser->m_nSpecifiedAtts = 0;
parser->m_unknownEncodingMem = NULL;
+ parser->m_unknownEncodingConvert = NULL;
parser->m_unknownEncodingRelease = NULL;
parser->m_unknownEncodingData = NULL;
parser->m_parsingStatus.parsing = XML_INITIALIZED;
@@ -1604,7 +1647,7 @@ XML_ParserReset(XML_Parser parser, const XML_Char *encodingName) {
moveToFreeBindingList(parser, parser->m_inheritedBindings);
FREE(parser, parser->m_unknownEncodingMem);
if (parser->m_unknownEncodingRelease)
- parser->m_unknownEncodingRelease(parser->m_unknownEncodingData);
+ callUnknownEncodingRelease(parser);
poolClear(&parser->m_tempPool);
poolClear(&parser->m_temp2Pool);
FREE(parser, (void *)parser->m_protocolEncodingName);
@@ -1915,7 +1958,7 @@ XML_ParserFree(XML_Parser parser) {
FREE(parser, parser->m_nsAtts);
FREE(parser, parser->m_unknownEncodingMem);
if (parser->m_unknownEncodingRelease)
- parser->m_unknownEncodingRelease(parser->m_unknownEncodingData);
+ callUnknownEncodingRelease(parser);
FREE(parser, parser);
}
@@ -2739,7 +2782,7 @@ XML_GetCurrentLineNumber(XML_Parser parser) {
parser->m_eventPtr, &parser->m_position);
parser->m_positionPtr = parser->m_eventPtr;
}
- // NOTE: XML_Size is known to wrap around for >2 4iB content
+ // NOTE: XML_Size is known to wrap around for >4 GiB content
// on 32bit machines and 64bit Windows, unless (non-default and
// uncommon) XML_LARGE_SIZE is defined.
// That's a bug and it only lives on because we cannot break
@@ -2756,7 +2799,7 @@ XML_GetCurrentColumnNumber(XML_Parser parser) {
parser->m_eventPtr, &parser->m_position);
parser->m_positionPtr = parser->m_eventPtr;
}
- // NOTE: XML_Size is known to wrap around for >2 4iB content
+ // NOTE: XML_Size is known to wrap around for >4 GiB content
// on 32bit machines and 64bit Windows, unless (non-default and
// uncommon) XML_LARGE_SIZE is defined.
// That's a bug and it only lives on because we cannot break
@@ -3410,9 +3453,9 @@ doContent(XML_Parser parser, int startTagLevel, const ENCODING *enc,
return result;
} else if (parser->m_externalEntityRefHandler) {
const XML_Char *context;
- entity->open = XML_TRUE;
+ entity->open = true;
context = getContext(parser);
- entity->open = XML_FALSE;
+ entity->open = false;
if (! context)
return XML_ERROR_NO_MEMORY;
beforeHandler(parser);
@@ -3837,8 +3880,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;
}
@@ -3951,11 +3994,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;
}
}
@@ -4046,8 +4092,8 @@ storeAtts(XML_Parser parser, const ENCODING *enc, const char *attStr,
unsigned int nsAttsSize = 1u << parser->m_nsAttsPower;
unsigned char oldNsAttsPower = parser->m_nsAttsPower;
/* size of hash table must be at least 2 * (# of prefixed attributes) */
- if ((nPrefixes << 1)
- >> parser->m_nsAttsPower) { /* true for m_nsAttsPower = 0 */
+ if (parser->m_nsAttsPower == 0
+ || (nPrefixes >> (parser->m_nsAttsPower - 1))) {
/* hash table size must also be a power of 2 and >= 8 */
while (nPrefixes >> parser->m_nsAttsPower++)
;
@@ -4946,25 +4992,34 @@ handleUnknownEncoding(XML_Parser parser, const XML_Char *encodingName) {
const int status = parser->m_unknownEncodingHandler(
parser->m_unknownEncodingHandlerData, encodingName, &info);
afterHandler(parser);
+
+ parser->m_unknownEncodingRelease = info.release;
+ parser->m_unknownEncodingData = info.data;
+
if (status) {
ENCODING *enc;
parser->m_unknownEncodingMem = MALLOC(parser, XmlSizeOfUnknownEncoding());
if (! parser->m_unknownEncodingMem) {
- if (info.release)
- info.release(info.data);
+ if (parser->m_unknownEncodingRelease)
+ callUnknownEncodingRelease(parser);
+ else
+ parser->m_unknownEncodingData = NULL;
return XML_ERROR_NO_MEMORY;
}
+ parser->m_unknownEncodingConvert = info.convert;
enc = (parser->m_ns ? XmlInitUnknownEncodingNS : XmlInitUnknownEncoding)(
- parser->m_unknownEncodingMem, info.map, info.convert, info.data);
+ parser->m_unknownEncodingMem, info.map,
+ info.convert ? callUnknownEncodingConvert : NULL, parser);
if (enc) {
- parser->m_unknownEncodingData = info.data;
- parser->m_unknownEncodingRelease = info.release;
parser->m_encoding = enc;
return XML_ERROR_NONE;
}
+ parser->m_unknownEncodingConvert = NULL;
}
- if (info.release != NULL)
- info.release(info.data);
+ if (parser->m_unknownEncodingRelease != NULL)
+ callUnknownEncodingRelease(parser);
+ else
+ parser->m_unknownEncodingData = NULL;
}
return XML_ERROR_UNKNOWN_ENCODING;
}
@@ -6092,7 +6147,7 @@ doProlog(XML_Parser parser, const ENCODING *enc, const char *s, const char *end,
}
if (parser->m_externalEntityRefHandler) {
dtd->paramEntityRead = XML_FALSE;
- entity->open = XML_TRUE;
+ entity->open = true;
entityTrackingOnOpen(parser, entity, __LINE__);
beforeHandler(parser);
const int status = parser->m_externalEntityRefHandler(
@@ -6101,11 +6156,11 @@ doProlog(XML_Parser parser, const ENCODING *enc, const char *s, const char *end,
afterHandler(parser);
if (! status) {
entityTrackingOnClose(parser, entity, __LINE__);
- entity->open = XML_FALSE;
+ entity->open = false;
return XML_ERROR_EXTERNAL_ENTITY_HANDLING;
}
entityTrackingOnClose(parser, entity, __LINE__);
- entity->open = XML_FALSE;
+ entity->open = false;
handleDefault = XML_FALSE;
if (! dtd->paramEntityRead) {
dtd->keepProcessing = dtd->standalone;
@@ -6429,7 +6484,7 @@ processEntity(XML_Parser parser, ENTITY *entity, XML_Bool betweenDecl,
if (! openEntity)
return XML_ERROR_NO_MEMORY;
}
- entity->open = XML_TRUE;
+ entity->open = true;
entity->hasMore = XML_TRUE;
#if XML_GE == 1
entityTrackingOnOpen(parser, entity, __LINE__);
@@ -6520,7 +6575,7 @@ internalEntityProcessor(XML_Parser parser, const char *s, const char *end,
// to false. This means we can directly remove the head of
// m_openInternalEntities
assert(parser->m_openInternalEntities == openEntity);
- entity->open = XML_FALSE;
+ entity->open = false;
parser->m_openInternalEntities = parser->m_openInternalEntities->next;
/* put openEntity back in list of free instances */
@@ -6598,7 +6653,7 @@ storeAttributeValue(XML_Parser parser, const ENCODING *enc, XML_Bool isCdata,
// with hasMore set to false. This means we can directly remove the head
// of m_openAttributeEntities
assert(parser->m_openAttributeEntities == openEntity);
- entity->open = XML_FALSE;
+ entity->open = false;
parser->m_openAttributeEntities = parser->m_openAttributeEntities->next;
/* put openEntity back in list of free instances */
@@ -6894,7 +6949,7 @@ storeEntityValue(XML_Parser parser, const ENCODING *enc,
if (entity->systemId) {
if (parser->m_externalEntityRefHandler) {
dtd->paramEntityRead = XML_FALSE;
- entity->open = XML_TRUE;
+ entity->open = true;
entityTrackingOnOpen(parser, entity, __LINE__);
beforeHandler(parser);
const int status = parser->m_externalEntityRefHandler(
@@ -6903,12 +6958,12 @@ storeEntityValue(XML_Parser parser, const ENCODING *enc,
afterHandler(parser);
if (! status) {
entityTrackingOnClose(parser, entity, __LINE__);
- entity->open = XML_FALSE;
+ entity->open = false;
result = XML_ERROR_EXTERNAL_ENTITY_HANDLING;
goto endEntityValue;
}
entityTrackingOnClose(parser, entity, __LINE__);
- entity->open = XML_FALSE;
+ entity->open = false;
if (! dtd->paramEntityRead)
dtd->keepProcessing = dtd->standalone;
} else
@@ -7058,7 +7113,7 @@ callStoreEntityValue(XML_Parser parser, const ENCODING *enc,
// with hasMore set to false. This means we can directly remove the head
// of m_openValueEntities
assert(parser->m_openValueEntities == openEntity);
- entity->open = XML_FALSE;
+ entity->open = false;
parser->m_openValueEntities = parser->m_openValueEntities->next;
/* put openEntity back in list of free instances */
@@ -7239,7 +7294,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)
@@ -7275,11 +7330,24 @@ defineAttribute(ELEMENT_TYPE *type, ATTRIBUTE_ID *attId, XML_Bool isCdata,
if (! isCdata)
attId->maybeTokenized = XML_TRUE;
- NAMED *const nameAddedOrFound
- = lookup(parser, &(type->defaultAttsNames), 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;
}
@@ -7480,7 +7548,7 @@ setContext(XML_Parser parser, const XML_Char *context) {
e = (ENTITY *)lookup(parser, &dtd->generalEntities,
poolStart(&parser->m_tempPool), 0);
if (e)
- e->open = XML_TRUE;
+ e->open = true;
if (*s != XML_T('\0'))
s++;
context = s;
@@ -7597,7 +7665,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 +7707,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 +7800,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,11 +7834,22 @@ dtdCopy(XML_Parser oldParser, DTD *newDtd, const DTD *oldDtd,
} else
newE->defaultAtts[i].value = NULL;
- NAMED *const nameAddedOrFound = lookup(parser, &(newE->defaultAttsNames),
- 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;
+ }
}
}
@@ -7867,19 +7946,23 @@ copyEntityTable(XML_Parser oldParser, HASH_TABLE *newTable,
#define INIT_POWER 6
+// Compares two strings `s1` and `s2` whereas:
+// - `s2` is zero-terminated but
+// - `s1` is made up of exactly (not just up to) `s1len` non-zero characters.
static XML_Bool FASTCALL
-keyeq(KEY s1, KEY s2) {
+keyeq(KEY s1, size_t s1len, KEY s2) {
#ifdef XML_UNICODE
# ifdef XML_UNICODE_WCHAR_T
- return (wcscmp(s1, s2) == 0) ? XML_TRUE : XML_FALSE;
+ return (wcsncmp(s1, s2, s1len) == 0 && s2[s1len] == L'\0') ? XML_TRUE
+ : XML_FALSE;
# else
- for (; *s1 == *s2; s1++, s2++)
- if (*s1 == 0)
- return XML_TRUE;
- return XML_FALSE;
+ for (; s1len > 0 && *s1 == *s2; s1len--, s1++, s2++)
+ ; /* no loop body! */
+ return ((s1len == 0) && (*s2 == 0)) ? XML_TRUE : XML_FALSE;
# endif
#else
- return (strcmp(s1, s2) == 0) ? XML_TRUE : XML_FALSE;
+ return (strncmp(s1, s2, s1len) == 0 && s2[s1len] == '\0') ? XML_TRUE
+ : XML_FALSE;
#endif
}
@@ -7897,18 +7980,38 @@ copy_salt_to_sipkey(XML_Parser parser, struct sipkey *key) {
}
static unsigned long FASTCALL
-hash(XML_Parser parser, KEY s) {
+hash(XML_Parser parser, KEY s, size_t keyLen) {
struct siphash state;
struct sipkey key;
(void)sip24_valid;
copy_salt_to_sipkey(parser, &key);
sip24_init(&state, &key);
- sip24_update(&state, s, keylen(s) * sizeof(XML_Char));
+ sip24_update(&state, s, keyLen * sizeof(XML_Char));
return (unsigned long)sip24_final(&state);
}
+// Function `lookupWithLength` can be used to either…
+//
+// a) check whether an element with key `name` exists in the given hash table
+// (read-only mode where `createSize == 0`) or
+//
+// b) check whether an element with key `name` exists in the given hash table
+// *and* insert it if missing (i.e. read-write mode where `createSize != 0`.
+//
+// When inserting, a block of `createSize` number of bytes will be allocated
+// and set to zero, and the resulting block of memory will be considered
+// to start with a `NAMED` structure, and `->name = name;` is performed.
+// The fact that all other bytes in the structure are initially zero can
+// be used to tell cases "existed and found" and "newly inserted" apart
+// with the structure returned.
+//
+// NOTE: Read-only lookup does not need zero-terminated keys but
+// read-write mode does, because keys can be re-hashed later and the
+// hash table does not store key length information.
+//
static NAMED *
-lookup(XML_Parser parser, HASH_TABLE *table, KEY name, size_t createSize) {
+lookupWithLength(XML_Parser parser, HASH_TABLE *table, KEY name, size_t nameLen,
+ size_t createSize) {
size_t i;
if (table->size == 0) {
size_t tsize;
@@ -7924,14 +8027,14 @@ lookup(XML_Parser parser, HASH_TABLE *table, KEY name, size_t createSize) {
return NULL;
}
memset(table->v, 0, tsize);
- i = hash(parser, name) & ((unsigned long)table->size - 1);
+ i = hash(parser, name, nameLen) & ((unsigned long)table->size - 1);
} else {
- unsigned long h = hash(parser, name);
+ unsigned long h = hash(parser, name, nameLen);
unsigned long mask = (unsigned long)table->size - 1;
unsigned char step = 0;
i = h & mask;
while (table->v[i]) {
- if (keyeq(name, table->v[i]->name))
+ if (keyeq(name, nameLen, table->v[i]->name))
return table->v[i];
if (! step)
step = PROBE_STEP(h, mask, table->power);
@@ -7964,7 +8067,8 @@ lookup(XML_Parser parser, HASH_TABLE *table, KEY name, size_t createSize) {
memset(newV, 0, tsize);
for (i = 0; i < table->size; i++)
if (table->v[i]) {
- unsigned long newHash = hash(parser, table->v[i]->name);
+ KEY const key = table->v[i]->name;
+ unsigned long newHash = hash(parser, key, keylen(key));
size_t j = newHash & newMask;
step = 0;
while (newV[j]) {
@@ -7987,15 +8091,36 @@ lookup(XML_Parser parser, HASH_TABLE *table, KEY name, size_t createSize) {
}
}
}
+ assert(createSize >= sizeof(NAMED));
table->v[i] = MALLOC(table->parser, createSize);
if (! table->v[i])
return NULL;
memset(table->v[i], 0, createSize);
- table->v[i]->name = name;
+ table->v[i]->name = name; // NOTE: This requires and assumes zero termination!
(table->used)++;
return table->v[i];
}
+// Function `lookup` can be used to either…
+//
+// a) check whether an element with key `name` exists in the given hash table
+// (read-only mode where `createSize == 0`) or
+//
+// b) check whether an element with key `name` exists in the given hash table
+// *and* insert it if missing (i.e. read-write mode where `createSize != 0`.
+//
+// When inserting, a block of `createSize` number of bytes will be allocated
+// and set to zero, and the resulting block of memory will be considered
+// to start with a `NAMED` structure, and `->name = name;` is performed.
+// The fact that all other bytes in the structure are initially zero can
+// be used to tell cases "existed and found" and "newly inserted" apart
+// with the structure returned.
+//
+static NAMED *
+lookup(XML_Parser parser, HASH_TABLE *table, KEY name, size_t createSize) {
+ return lookupWithLength(parser, table, name, keylen(name), createSize);
+}
+
static void FASTCALL
hashTableClear(HASH_TABLE *table) {
size_t i;
@@ -8535,8 +8660,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 {
diff --git a/lib/xmltok.h b/lib/xmltok.h
index bd868b87a407..76be2c7c5ca1 100644
--- a/lib/xmltok.h
+++ b/lib/xmltok.h
@@ -169,8 +169,8 @@ typedef int(PTRCALL *SCANNER)(const ENCODING *, const char *, const char *,
enum XML_Convert_Result {
XML_CONVERT_COMPLETED = 0,
XML_CONVERT_INPUT_INCOMPLETE = 1,
- XML_CONVERT_OUTPUT_EXHAUSTED
- = 2 /* and therefore potentially input remaining as well */
+ XML_CONVERT_OUTPUT_EXHAUSTED = 2 /* and therefore potentially input remaining
+ as well */
};
struct encoding {
diff --git a/tests/basic_tests.c b/tests/basic_tests.c
index 308adf6cbce0..e6829516ca79 100644
--- a/tests/basic_tests.c
+++ b/tests/basic_tests.c
@@ -23,6 +23,7 @@
Copyright (c) 2026 Francesco Bertolaccini
Copyright (c) 2026 Matthew Fernandez <matthew.fernandez@gmail.com>
Copyright (c) 2026 Kartik Kenchi <netliomax25@gmail.com>
+ Copyright (c) 2026 Zeyou Liu <zeyouliu@tencent.com>
Licensed under the MIT license:
Permission is hereby granted, free of charge, to any person obtaining
@@ -2809,6 +2810,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().
*/
@@ -3440,6 +3514,8 @@ START_TEST(test_buffer_can_grow_to_max) {
maxbuf = maxbuf / 2;
fprintf(stderr, "Reducing maxbuf to %d...\n", maxbuf);
}
+#else
+ UNUSED_P(maxbuf);
#endif
for (int i = 0; i < num_prefixes; ++i) {
@@ -3463,6 +3539,8 @@ START_TEST(test_buffer_can_grow_to_max) {
// The limit should be consistent; no prefix should allow us to
// reach above the max buffer size.
assert_true(XML_GetBuffer(parser, maxbuf + 1) == NULL);
+#else
+ UNUSED_P(maxbuf);
#endif
XML_ParserFree(parser);
@@ -6737,6 +6815,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);
diff --git a/tests/misc_tests.c b/tests/misc_tests.c
index 82b4b54b15bf..889620d42660 100644
--- a/tests/misc_tests.c
+++ b/tests/misc_tests.c
@@ -23,6 +23,7 @@
Copyright (c) 2026 Matthew Fernandez <matthew.fernandez@gmail.com>
Copyright (c) 2026 Kartik Kenchi <netliomax25@gmail.com>
Copyright (c) 2026 Evgeny Kotkov <kotkov@apache.org>
+ Copyright (c) 2026 Darren Carreras <carrerasdarren@gmail.com>
Licensed under the MIT license:
Permission is hereby granted, free of charge, to any person obtaining
@@ -217,7 +218,7 @@ START_TEST(test_misc_version) {
if (! versions_equal(&read_version, &parsed_version))
fail("Version mismatch");
- if (xcstrcmp(version_text, XCS("expat_2.8.3"))
+ if (xcstrcmp(version_text, XCS("expat_2.8.4"))
!= 0) /* needs bump on releases */
fail("XML_*_VERSION in expat.h out of sync?\n");
}
@@ -839,6 +840,60 @@ START_TEST(test_misc_resume_parser_forbidden_from_handler) {
}
END_TEST
+typedef struct {
+ XML_Parser parser;
+ int converterCallCount;
+ int releaseCallCount;
+} EncodingCallbackData;
+
+static int XMLCALL
+reentrant_encoding_converter(void *userData, const char *s) {
+ EncodingCallbackData *const data = userData;
+ UNUSED_P(s);
+ data->converterCallCount++;
+ forbidden_calls_character_handler(data->parser, NULL, 0);
+ return 'A';
+}
+
+static void XMLCALL
+reentrant_encoding_release(void *userData) {
+ EncodingCallbackData *const data = userData;
+ data->releaseCallCount++;
+ forbidden_calls_character_handler(data->parser, NULL, 0);
+}
+
+static int XMLCALL
+reentrant_encoding_handler(void *userData, const XML_Char *name,
+ XML_Encoding *info) {
+ EncodingCallbackData *const data = userData;
+ UNUSED_P(name);
+
+ for (int i = 0; i < 256; i++)
+ info->map[i] = i;
+ info->map[0x80] = -2; // Route byte 0x80 through the custom converter.
+ info->data = data;
+ info->convert = reentrant_encoding_converter;
+ info->release = reentrant_encoding_release;
+ return XML_STATUS_OK;
+}
+
+START_TEST(test_misc_unknown_encoding_callbacks_protected) {
+ const char *const doc
+ = "<?xml version='1.0' encoding='reentrant-conv'?><doc>\x80\x80</doc>";
+ XML_Parser parser = XML_ParserCreate(NULL);
+ EncodingCallbackData data = {parser, 0, 0};
+ XML_SetUnknownEncodingHandler(parser, reentrant_encoding_handler, &data);
+
+ assert_true(XML_Parse(parser, doc, (int)strlen(doc), /*isFinal=*/XML_TRUE)
+ == XML_STATUS_OK);
+ assert_true(data.converterCallCount > 0);
+ assert_true(data.releaseCallCount == 0); // Released by XML_ParserFree below.
+
+ XML_ParserFree(parser);
+ assert_true(data.releaseCallCount == 1);
+}
+END_TEST
+
// General attack payload idea by Jason Kratzer of Mozilla
START_TEST(test_misc_low_surrogate_mozilla_bug_2053153) {
const char doc_before[] = "<\0!\0D\0O\0C\0T\0Y\0P\0E\0 \0d\0 \0[\0\n\0"
@@ -936,6 +991,7 @@ make_miscellaneous_test_case(Suite *s) {
tcase_add_test(tc_misc, test_misc_no_infinite_loop_issue_1161);
tcase_add_test(tc_misc, test_misc_calls_forbidden_from_handlers);
tcase_add_test(tc_misc, test_misc_resume_parser_forbidden_from_handler);
+ tcase_add_test(tc_misc, test_misc_unknown_encoding_callbacks_protected);
tcase_add_test(tc_misc, test_misc_input_2gb);
tcase_add_test(tc_misc, test_misc_low_surrogate_mozilla_bug_2053153);
}