diff options
| author | Philip Paeps <philip@FreeBSD.org> | 2026-08-31 15:36:27 +0000 |
|---|---|---|
| committer | Philip Paeps <philip@FreeBSD.org> | 2026-08-31 15:36:27 +0000 |
| commit | 698f424711732909a9b349950543e220eb0dfa08 (patch) | |
| tree | 3a44d992e311df8ff4f92dc5ebdf43754249dc80 | |
| parent | ef5aa86648f5120e7badb14ea8a6cb2a312fc56b (diff) | |
Vendor import of expat 2.8.4vendor/expat/2.8.4vendor/expat
| -rw-r--r-- | Changes | 65 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | configure.ac | 2 | ||||
| -rw-r--r-- | doc/reference.html | 2 | ||||
| -rw-r--r-- | doc/xmlwf.1 | 2 | ||||
| -rw-r--r-- | doc/xmlwf.xml | 2 | ||||
| -rwxr-xr-x | fix-xmltest-log.sh | 3 | ||||
| -rw-r--r-- | lib/expat.h | 2 | ||||
| -rw-r--r-- | lib/internal.h | 1 | ||||
| -rw-r--r-- | lib/random_getentropy.c | 2 | ||||
| -rw-r--r-- | lib/xmlparse.c | 253 | ||||
| -rw-r--r-- | lib/xmltok.h | 4 | ||||
| -rw-r--r-- | tests/basic_tests.c | 79 | ||||
| -rw-r--r-- | tests/misc_tests.c | 58 |
14 files changed, 403 insertions, 74 deletions
@@ -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); } |
