# erAck: backport of expat CVE-2023-52425 DoS fix # https://github.com/libexpat/libexpat/commit/34b598c5f594b015c513c73f06e7ced3323edbf1 # --- thunderbird-115.9.0/parser/expat/lib/expat.h.expat-CVE-2023-52425 2024-03-11 20:36:11.000000000 +0100 +++ thunderbird-115.9.0/parser/expat/lib/expat.h 2024-03-13 20:46:45.648505015 +0100 @@ -1045,6 +1045,10 @@ XMLPARSEAPI(const XML_Feature *) XML_GetFeatureList(void); +/* Added in Expat 2.6.0. */ +XMLPARSEAPI(XML_Bool) +XML_SetReparseDeferralEnabled(XML_Parser parser, XML_Bool enabled); + /* Expat follows the semantic versioning convention. See http://semver.org. */ --- thunderbird-115.9.0/parser/expat/lib/internal.h.expat-CVE-2023-52425 2024-03-11 20:36:11.000000000 +0100 +++ thunderbird-115.9.0/parser/expat/lib/internal.h 2024-03-14 00:14:39.334319725 +0100 @@ -80,6 +80,7 @@ # endif #endif +#include "expat.h" // so we can use type XML_Parser below #ifdef __cplusplus extern "C" { @@ -90,6 +91,9 @@ void align_limit_to_full_utf8_characters(const char * from, const char ** fromLimRef); +extern XML_Bool g_reparseDeferralEnabledDefault; // written ONLY in runtests.c +extern unsigned int g_parseAttempts; // used for testing only + #ifdef __cplusplus } #endif --- thunderbird-115.9.0/parser/expat/lib/xmlparse.c.expat-CVE-2023-52425 2024-03-11 20:36:11.000000000 +0100 +++ thunderbird-115.9.0/parser/expat/lib/xmlparse.c 2024-03-13 22:55:14.844756009 +0100 @@ -6,6 +6,7 @@ #define _GNU_SOURCE /* syscall prototype */ +#include #include #include /* memset(), memcpy() */ #include @@ -89,6 +90,9 @@ typedef char ICHAR; /* Round up n to be a multiple of sz, where sz is a power of 2. */ #define ROUND_UP(n, sz) (((n) + ((sz) - 1)) & ~((sz) - 1)) +/* Do safe (NULL-aware) pointer arithmetic */ +#define EXPAT_SAFE_PTR_DIFF(p, q) (((p) && (q)) ? ((p) - (q)) : 0) + /* Handle the case where memmove() doesn't exist. */ #ifndef HAVE_MEMMOVE #ifdef HAVE_BCOPY @@ -98,6 +102,8 @@ typedef char ICHAR; #endif /* HAVE_BCOPY */ #endif /* HAVE_MEMMOVE */ +#define EXPAT_MIN(a, b) (((a) < (b)) ? (a) : (b)) + #include "internal.h" #include "xmltok.h" #include "xmlrole.h" @@ -476,6 +482,9 @@ parserInit(XML_Parser parser, const XML_ ? 0 \ : ((*((pool)->ptr)++ = c), 1)) +XML_Bool g_reparseDeferralEnabledDefault = XML_TRUE; // write ONLY in runtests.c +unsigned int g_parseAttempts = 0; // used for testing only + struct XML_ParserStruct { /* The first member must be userData so that the XML_GetUserData macro works. */ @@ -491,6 +500,9 @@ struct XML_ParserStruct { const char *m_bufferLim; XML_Index m_parseEndByteIndex; const char *m_parseEndPtr; + size_t m_partialTokenBytesBefore; /* used in heuristic to avoid O(n^2) */ + XML_Bool m_reparseDeferralEnabled; + int m_lastBufferRequestSize; XML_Char *m_dataBuf; XML_Char *m_dataBufEnd; XML_StartElementHandler m_startElementHandler; @@ -647,6 +659,9 @@ struct XML_ParserStruct { #define bufferEnd (parser->m_bufferEnd) #define parseEndByteIndex (parser->m_parseEndByteIndex) #define parseEndPtr (parser->m_parseEndPtr) +#define partialTokenBytesBefore (parser->m_partialTokenBytesBefore) +#define reparseDeferralEnabled (parser->m_reparseDeferralEnabled) +#define lastBufferRequestSize (parser->m_lastBufferRequestSize) #define bufferLim (parser->m_bufferLim) #define dataBuf (parser->m_dataBuf) #define dataBufEnd (parser->m_dataBufEnd) @@ -887,6 +902,47 @@ get_hash_secret_salt(XML_Parser parser) return parser->m_hash_secret_salt; } +static enum XML_Error +callProcessor(XML_Parser parser, const char *start, const char *end, + const char **endPtr) { + const size_t have_now = EXPAT_SAFE_PTR_DIFF(end, start); + + if (parser->m_reparseDeferralEnabled + && ! parser->m_parsingStatus.finalBuffer) { + // Heuristic: don't try to parse a partial token again until the amount of + // available data has increased significantly. + const size_t had_before = parser->m_partialTokenBytesBefore; + // ...but *do* try anyway if we're close to causing a reallocation. + size_t available_buffer + = EXPAT_SAFE_PTR_DIFF(parser->m_bufferPtr, parser->m_buffer); +#if XML_CONTEXT_BYTES > 0 + available_buffer -= EXPAT_MIN(available_buffer, XML_CONTEXT_BYTES); +#endif + available_buffer + += EXPAT_SAFE_PTR_DIFF(parser->m_bufferLim, parser->m_bufferEnd); + // m_lastBufferRequestSize is never assigned a value < 0, so the cast is ok + const bool enough + = (have_now >= 2 * had_before) + || ((size_t)parser->m_lastBufferRequestSize > available_buffer); + + if (! enough) { + *endPtr = start; // callers may expect this to be set + return XML_ERROR_NONE; + } + } + g_parseAttempts += 1; + const enum XML_Error ret = parser->m_processor(parser, start, end, endPtr); + if (ret == XML_ERROR_NONE) { + // if we consumed nothing, remember what we had on this parse attempt. + if (*endPtr == start) { + parser->m_partialTokenBytesBefore = have_now; + } else { + parser->m_partialTokenBytesBefore = 0; + } + } + return ret; +} + static XML_Bool /* only valid for root parser */ startParsing(XML_Parser parser) { @@ -1075,6 +1131,9 @@ parserInit(XML_Parser parser, const XML_ bufferEnd = buffer; parseEndByteIndex = 0; parseEndPtr = NULL; + partialTokenBytesBefore = 0; + reparseDeferralEnabled = g_reparseDeferralEnabledDefault; + lastBufferRequestSize = 0; declElementType = NULL; declAttributeId = NULL; declEntity = NULL; @@ -1232,6 +1291,7 @@ XML_ExternalEntityParserCreate(XML_Parse to worry which hash secrets each table has. */ unsigned long oldhash_secret_salt; + XML_Bool oldReparseDeferralEnabled; /* Validate the oldParser parameter before we pull everything out of it */ if (oldParser == NULL) @@ -1276,6 +1336,7 @@ XML_ExternalEntityParserCreate(XML_Parse to worry which hash secrets each table has. */ oldhash_secret_salt = hash_secret_salt; + oldReparseDeferralEnabled = reparseDeferralEnabled; #ifdef XML_DTD if (!context) @@ -1330,6 +1391,7 @@ XML_ExternalEntityParserCreate(XML_Parse defaultExpandInternalEntities = oldDefaultExpandInternalEntities; ns_triplets = oldns_triplets; hash_secret_salt = oldhash_secret_salt; + reparseDeferralEnabled = oldReparseDeferralEnabled; parentParser = oldParser; #ifdef XML_DTD paramEntityParsing = oldParamEntityParsing; @@ -1850,39 +1912,8 @@ XML_Parse(XML_Parser parser, const char ps_parsing = XML_PARSING; } - if (len == 0) { - ps_finalBuffer = (XML_Bool)isFinal; - if (!isFinal) - return XML_STATUS_OK; - positionPtr = bufferPtr; - parseEndPtr = bufferEnd; - - /* If data are left over from last buffer, and we now know that these - data are the final chunk of input, then we have to check them again - to detect errors based on that fact. - */ - errorCode = processor(parser, bufferPtr, parseEndPtr, &bufferPtr); - - if (errorCode == XML_ERROR_NONE) { - switch (ps_parsing) { - case XML_SUSPENDED: - XmlUpdatePosition(encoding, positionPtr, bufferPtr, &position); - positionPtr = bufferPtr; - return XML_STATUS_SUSPENDED; - case XML_INITIALIZED: - case XML_PARSING: - ps_parsing = XML_FINISHED; - /* fall through */ - default: - return XML_STATUS_OK; - } - } - eventEndPtr = eventPtr; - processor = errorProcessor; - return XML_STATUS_ERROR; - } #ifndef XML_CONTEXT_BYTES - else if (bufferPtr == bufferEnd) { + if (bufferPtr == bufferEnd) { const char *end; int nLeftOver; enum XML_Status result; @@ -1899,11 +1930,14 @@ XML_Parse(XML_Parser parser, const char processor = errorProcessor; return XML_STATUS_ERROR; } + // though this isn't a buffer request, we assume that `len` is the app's + // preferred buffer fill size, and therefore save it here. + lastBufferRequestSize = len; parseEndByteIndex += len; positionPtr = s; ps_finalBuffer = (XML_Bool)isFinal; - errorCode = processor(parser, s, parseEndPtr = s + len, &end); + errorCode = callProcessor(parser, s, parseEndPtr = s + len, &end); if (errorCode != XML_ERROR_NONE) { eventEndPtr = eventPtr; @@ -1930,6 +1964,8 @@ XML_Parse(XML_Parser parser, const char XmlUpdatePosition(encoding, positionPtr, end, &position); nLeftOver = s + len - end; if (nLeftOver) { +#if 0 +// erAck: replace with XML_GetBuffer() below. if (buffer == NULL || nLeftOver > bufferLim - buffer) { /* avoid _signed_ integer overflow */ char *temp = NULL; @@ -1939,6 +1975,28 @@ XML_Parse(XML_Parser parser, const char ? (char *)MALLOC(bytesToAllocate) : (char *)REALLOC(buffer, bytesToAllocate)); } +#endif +#if 1 +// erAck: the original patch context had a call to XML_GetBuffer() instead: + // Back up and restore the parsing status to avoid XML_ERROR_SUSPENDED + // (and XML_ERROR_FINISHED) from XML_GetBuffer. + const enum XML_Parsing originalStatus = ps_parsing; + ps_parsing = XML_PARSING; + void *const temp = XML_GetBuffer(parser, nLeftOver); + ps_parsing = originalStatus; +#endif + // GetBuffer may have overwritten this, but we want to remember what the + // app requested, not how many bytes were left over after parsing. + lastBufferRequestSize = len; +#if 1 + if (temp == NULL) { + // NOTE: parser->m_errorCode has already been set by XML_GetBuffer(). + eventPtr = eventEndPtr = NULL; + processor = errorProcessor; + return XML_STATUS_ERROR; + } +#endif +#if 0 if (temp == NULL) { errorCode = XML_ERROR_NO_MEMORY; eventPtr = eventEndPtr = NULL; @@ -1948,6 +2006,7 @@ XML_Parse(XML_Parser parser, const char buffer = temp; bufferLim = buffer + bytesToAllocate; } +#endif memcpy(buffer, end, nLeftOver); } bufferPtr = buffer; @@ -1959,15 +2018,14 @@ XML_Parse(XML_Parser parser, const char return result; } #endif /* not defined XML_CONTEXT_BYTES */ - else { - void *buff = XML_GetBuffer(parser, len); - if (buff == NULL) - return XML_STATUS_ERROR; - else { - memcpy(buff, s, len); - return XML_ParseBuffer(parser, len, isFinal); - } + void *buff = XML_GetBuffer(parser, len); + if (buff == NULL) + return XML_STATUS_ERROR; + if (len > 0) { + assert(s != NULL); // make sure s==NULL && len!=0 was rejected above + memcpy(buff, s, len); } + return XML_ParseBuffer(parser, len, isFinal); } enum XML_Status XMLCALL @@ -2001,7 +2059,7 @@ XML_ParseBuffer(XML_Parser parser, int l parseEndByteIndex += len; ps_finalBuffer = (XML_Bool)isFinal; - errorCode = processor(parser, start, parseEndPtr, &bufferPtr); + errorCode = callProcessor(parser, start, parseEndPtr, &bufferPtr); if (errorCode != XML_ERROR_NONE) { eventEndPtr = eventPtr; @@ -2047,7 +2105,11 @@ XML_GetBuffer(XML_Parser parser, int len default: ; } - if (len > bufferLim - bufferEnd) { + // whether or not the request succeeds, `len` seems to be the app's preferred + // buffer fill size; remember it. + lastBufferRequestSize = len; + if (len > EXPAT_SAFE_PTR_DIFF(bufferLim, bufferEnd) + || buffer == NULL) { #ifdef XML_CONTEXT_BYTES int keep; #endif /* defined XML_CONTEXT_BYTES */ @@ -2063,7 +2125,9 @@ XML_GetBuffer(XML_Parser parser, int len keep = XML_CONTEXT_BYTES; neededSize += keep; #endif /* defined XML_CONTEXT_BYTES */ - if (neededSize <= bufferLim - buffer) { + if (buffer && bufferPtr + && neededSize + <= EXPAT_SAFE_PTR_DIFF(bufferLim, buffer)) { #ifdef XML_CONTEXT_BYTES if (keep < bufferPtr - buffer) { int offset = (int)(bufferPtr - buffer) - keep; @@ -2072,8 +2136,11 @@ XML_GetBuffer(XML_Parser parser, int len bufferPtr -= offset; } #else - memmove(buffer, bufferPtr, bufferEnd - bufferPtr); - bufferEnd = buffer + (bufferEnd - bufferPtr); + memmove(buffer, bufferPtr, + EXPAT_SAFE_PTR_DIFF(bufferEnd, bufferPtr)); + bufferEnd + = buffer + + EXPAT_SAFE_PTR_DIFF(bufferEnd, bufferPtr); bufferPtr = buffer; #endif /* not defined XML_CONTEXT_BYTES */ } @@ -2171,7 +2238,7 @@ XML_ResumeParser(XML_Parser parser) } ps_parsing = XML_PARSING; - errorCode = processor(parser, bufferPtr, parseEndPtr, &bufferPtr); + errorCode = callProcessor(parser, bufferPtr, parseEndPtr, &bufferPtr); if (errorCode != XML_ERROR_NONE) { eventEndPtr = eventPtr; @@ -2481,6 +2548,15 @@ MOZ_XML_ProcessingEntityValue(XML_Parser } /* END MOZILLA CHANGE */ +XML_Bool XMLCALL +XML_SetReparseDeferralEnabled(XML_Parser parser, XML_Bool enabled) { + if (parser != NULL && (enabled == XML_TRUE || enabled == XML_FALSE)) { + parser->m_reparseDeferralEnabled = enabled; + return XML_TRUE; + } + return XML_FALSE; +} + /* Initially tag->rawName always points into the parse buffer; for those TAG instances opened while the current parse buffer was processed, and not yet closed, we need to store tag->rawName in a more