untrusted comment: verify with openbsd-78-base.pub RWS3/nvFmk4SWfVMXzdA3RIaFh0epKLOtNS+sxzCoKMfFe9hLGIMJR5hj8pq2kFEXAt2TNzC6MPRvED3pSoLx0IFsUodtJn03gQ= OpenBSD 7.8 errata 055, September 14, 2026: Backport all changes from libexpat 2.8.4. CVE-2026-66046 CVE-2026-76641 CVE-2026-76957 Apply by doing: signify -Vep /etc/signify/openbsd-78-base.pub -x 055_expat.patch.sig \ -m - | (cd /usr/src && patch -p0) And then rebuild and install libexpat: cd /usr/src/lib/libexpat make obj make make install Index: lib/libexpat/Changes =================================================================== RCS file: /cvs/src/lib/libexpat/Changes,v diff -u -p -r1.32.2.4 Changes --- lib/libexpat/Changes 20 Aug 2026 07:47:56 -0000 1.32.2.4 +++ lib/libexpat/Changes 7 Sep 2026 12:27:33 -0000 @@ -16,6 +16,71 @@ !! Sebastian Pipping -- Berlin, 2026-08-03 !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +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 + Mon August 10 2026 Security fixes: #1296 CVE-2026-72522 -- Fix an out-of-bounds read and the resulting Index: lib/libexpat/lib/internal.h =================================================================== RCS file: /cvs/src/lib/libexpat/lib/internal.h,v diff -u -p -r1.15.2.2 internal.h --- lib/libexpat/lib/internal.h 20 Aug 2026 07:47:56 -0000 1.15.2.2 +++ lib/libexpat/lib/internal.h 7 Sep 2026 12:27:33 -0000 @@ -33,6 +33,7 @@ Copyright (c) 2019 David Loffredo Copyright (c) 2023-2024 Sony Corporation / Snild Dolkow Copyright (c) 2024 Taichi Haradaguchi <20001722@ymail.ne.jp> + Copyright (c) 2026 Matthew Wozniczka Licensed under the MIT license: Permission is hereby granted, free of charge, to any person obtaining Index: lib/libexpat/lib/xmlparse.c =================================================================== RCS file: /cvs/src/lib/libexpat/lib/xmlparse.c,v diff -u -p -r1.44.2.4 xmlparse.c --- lib/libexpat/lib/xmlparse.c 20 Aug 2026 07:47:56 -0000 1.44.2.4 +++ lib/libexpat/lib/xmlparse.c 7 Sep 2026 12:27:34 -0000 @@ -1,4 +1,4 @@ -/* ee5f82c3ffd57c5224394ba46f348dbce466d34d6c925a527ae46b1cfe6adf1d (2.8.3+) +/* 13c4e8da8fccffb0e8e599684e0d447ad14c1bb0b48792cf5dd77d8712301871 (2.8.4+) __ __ _ ___\ \/ /_ __ __ _| |_ / _ \\ /| '_ \ / _` | __| @@ -51,6 +51,9 @@ Copyright (c) 2026 Kartik Kenchi Copyright (c) 2026 Haris Hussain Copyright (c) 2026 Evgeny Kotkov + Copyright (c) 2026 Darren Carreras + Copyright (c) 2026 Alberto Maschietto + Copyright (c) 2026 Zeyou Liu Licensed under the MIT license: Permission is hereby granted, free of charge, to any person obtaining @@ -306,7 +309,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 @@ -357,6 +360,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; @@ -370,7 +389,7 @@ typedef struct { size_t nDefaultAtts; size_t allocDefaultAtts; DEFAULT_ATTRIBUTE *defaultAtts; - HASH_TABLE defaultAttsNames; + HASH_TABLE defaultAttForName; } ELEMENT_TYPE; typedef struct { @@ -555,6 +574,8 @@ static int dtdCopy(XML_Parser oldParser, 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); @@ -731,6 +752,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; @@ -1153,6 +1176,25 @@ isCalledFromInsideHandler(XML_Parser par 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) { @@ -1500,6 +1542,7 @@ parserInit(XML_Parser parser, const XML_ 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; @@ -1580,7 +1623,7 @@ XML_ParserReset(XML_Parser parser, const 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); @@ -1891,7 +1934,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); } @@ -2690,7 +2733,7 @@ XML_GetCurrentLineNumber(XML_Parser pars 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 @@ -2707,7 +2750,7 @@ XML_GetCurrentColumnNumber(XML_Parser pa 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 @@ -3361,9 +3404,9 @@ doContent(XML_Parser parser, int startTa 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); @@ -3788,8 +3831,8 @@ storeAtts(XML_Parser parser, const ENCOD 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; } @@ -3902,11 +3945,14 @@ storeAtts(XML_Parser parser, const ENCOD /* 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; } } @@ -3997,8 +4043,8 @@ storeAtts(XML_Parser parser, const ENCOD 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++) ; @@ -4897,25 +4943,34 @@ handleUnknownEncoding(XML_Parser parser, 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; } @@ -6043,7 +6098,7 @@ doProlog(XML_Parser parser, const ENCODI } 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( @@ -6052,11 +6107,11 @@ doProlog(XML_Parser parser, const ENCODI 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; @@ -6380,7 +6435,7 @@ processEntity(XML_Parser parser, ENTITY 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__); @@ -6471,7 +6526,7 @@ internalEntityProcessor(XML_Parser parse // 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 */ @@ -6549,7 +6604,7 @@ storeAttributeValue(XML_Parser parser, c // 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 */ @@ -6845,7 +6900,7 @@ storeEntityValue(XML_Parser parser, cons 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( @@ -6854,12 +6909,12 @@ storeEntityValue(XML_Parser parser, cons 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 @@ -7009,7 +7064,7 @@ callStoreEntityValue(XML_Parser parser, // 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 */ @@ -7190,7 +7245,7 @@ defineAttribute(ELEMENT_TYPE *type, ATTR /* 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) @@ -7226,11 +7281,24 @@ defineAttribute(ELEMENT_TYPE *type, ATTR 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; } @@ -7431,7 +7499,7 @@ setContext(XML_Parser parser, const XML_ 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; @@ -7548,7 +7616,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)); @@ -7590,7 +7658,7 @@ dtdDestroy(DTD *p, XML_Bool isDocEntity, ELEMENT_TYPE *e = (ELEMENT_TYPE *)hashTableIterNext(&iter); if (! e) break; - hashTableDestroy(&(e->defaultAttsNames)); + hashTableDestroy(&(e->defaultAttForName)); FREE(parser, e->defaultAtts); } hashTableDestroy(&(p->generalEntities)); @@ -7683,8 +7751,8 @@ dtdCopy(XML_Parser oldParser, DTD *newDt 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. */ @@ -7717,11 +7785,22 @@ dtdCopy(XML_Parser oldParser, DTD *newDt } 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; + } } } @@ -7818,19 +7897,23 @@ copyEntityTable(XML_Parser oldParser, HA #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 } @@ -7848,18 +7931,38 @@ copy_salt_to_sipkey(XML_Parser parser, s } 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; @@ -7875,14 +7978,14 @@ lookup(XML_Parser parser, HASH_TABLE *ta 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); @@ -7915,7 +8018,8 @@ lookup(XML_Parser parser, HASH_TABLE *ta 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]) { @@ -7938,15 +8042,36 @@ lookup(XML_Parser parser, HASH_TABLE *ta } } } + 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; @@ -8486,8 +8611,8 @@ getElementType(XML_Parser parser, const 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 { Index: lib/libexpat/lib/xmltok.h =================================================================== RCS file: /cvs/src/lib/libexpat/lib/xmltok.h,v diff -u -p -r1.9.2.1 xmltok.h --- lib/libexpat/lib/xmltok.h 20 Aug 2026 07:47:56 -0000 1.9.2.1 +++ lib/libexpat/lib/xmltok.h 7 Sep 2026 12:27:34 -0000 @@ -169,8 +169,8 @@ typedef int(PTRCALL *SCANNER)(const ENCO 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 { Index: lib/libexpat/tests/basic_tests.c =================================================================== RCS file: /cvs/src/lib/libexpat/tests/basic_tests.c,v diff -u -p -r1.7.2.3 basic_tests.c --- lib/libexpat/tests/basic_tests.c 20 Aug 2026 07:47:56 -0000 1.7.2.3 +++ lib/libexpat/tests/basic_tests.c 7 Sep 2026 12:27:34 -0000 @@ -23,6 +23,7 @@ Copyright (c) 2026 Francesco Bertolaccini Copyright (c) 2026 Matthew Fernandez Copyright (c) 2026 Kartik Kenchi + Copyright (c) 2026 Zeyou Liu Licensed under the MIT license: Permission is hereby granted, free of charge, to any person obtaining @@ -2785,6 +2786,79 @@ START_TEST(test_duplicate_id_attribute_m } 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 = ""; + 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 = "\n" + " \n" + " \n" + " \n" + " \n" + "]>\n" + "&e;\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(). */ @@ -3416,6 +3490,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) { @@ -3439,6 +3515,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); @@ -6716,6 +6794,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); Index: lib/libexpat/tests/misc_tests.c =================================================================== RCS file: /cvs/src/lib/libexpat/tests/misc_tests.c,v diff -u -p -r1.8.2.2 misc_tests.c --- lib/libexpat/tests/misc_tests.c 20 Aug 2026 07:47:56 -0000 1.8.2.2 +++ lib/libexpat/tests/misc_tests.c 7 Sep 2026 12:27:34 -0000 @@ -23,6 +23,7 @@ Copyright (c) 2026 Matthew Fernandez Copyright (c) 2026 Kartik Kenchi Copyright (c) 2026 Evgeny Kotkov + Copyright (c) 2026 Darren Carreras Licensed under the MIT license: Permission is hereby granted, free of charge, to any person obtaining @@ -839,6 +840,60 @@ START_TEST(test_misc_resume_parser_forbi } 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 + = "\x80\x80"; + 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); }