From 3a8307d3e5b9df06f3acc4766fec32030d5d9e50 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Wed, 13 May 2026 11:29:00 +0200 Subject: [PATCH 1/6] Fix compiler warning with glibc 2.43 support of C23 const-preserving standard library macros: assignment discards 'const' qualifier from pointer target type [-Werror=discarded-qualifiers] Closes GH-22031 --- ext/dba/libinifile/inifile.c | 2 +- ext/readline/readline_cli.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ext/dba/libinifile/inifile.c b/ext/dba/libinifile/inifile.c index c5467396d4bf..4dc687ea6681 100644 --- a/ext/dba/libinifile/inifile.c +++ b/ext/dba/libinifile/inifile.c @@ -111,7 +111,7 @@ void inifile_free(inifile *dba, int persistent) key_type inifile_key_split(const char *group_name) { key_type key; - char *name; + const char *name; if (group_name[0] == '[' && (name = strchr(group_name, ']')) != NULL) { key.group = estrndup(group_name+1, name - (group_name + 1)); diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c index 312129991c70..2b0950784bf2 100644 --- a/ext/readline/readline_cli.c +++ b/ext/readline/readline_cli.c @@ -529,7 +529,8 @@ static char *cli_completion_generator(const char *text, int index) /* {{{ */ } else if (text[0] == '#' && text[1] != '[') { retval = cli_completion_generator_ini(text, textlen, &cli_completion_state); } else { - char *lc_text, *class_name_end; + char *lc_text; + const char *class_name_end; zend_string *class_name = NULL; zend_class_entry *ce = NULL; From bb739429b48130b0654fc460d4fee22e721b4cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 18 May 2026 16:48:28 +0200 Subject: [PATCH 2/6] uri: Update to uriparser-1.0.2 (#22070) This fixes CVE-2026-44927 and CVE-2026-44928. --- NEWS | 8 + ext/uri/uriparser/include/uriparser/Uri.h | 2 +- ext/uri/uriparser/include/uriparser/UriBase.h | 2 +- ext/uri/uriparser/src/UriCommon.c | 40 ++-- ext/uri/uriparser/src/UriCommon.h | 4 +- ext/uri/uriparser/src/UriCompare.c | 20 +- ext/uri/uriparser/src/UriCopy.c | 6 + ext/uri/uriparser/src/UriFile.c | 16 +- ext/uri/uriparser/src/UriNormalize.c | 23 ++- ext/uri/uriparser/src/UriQuery.c | 29 ++- ext/uri/uriparser/src/UriRecompose.c | 175 +++++++++++++++--- ext/uri/uriparser/src/UriResolve.c | 5 +- ext/uri/uriparser/src/UriShorten.c | 13 +- 13 files changed, 257 insertions(+), 86 deletions(-) diff --git a/NEWS b/NEWS index d0e3104f82da..7d4f5cde9fec 100644 --- a/NEWS +++ b/NEWS @@ -23,6 +23,14 @@ PHP NEWS . Fixed bug GH-21689 (version_compare() incorrectly handles versions ending with a dot). (timwolla) +- URI: + . Fixed CVE-2026-44927 (In uriparser before 1.0.2, there is pointer + difference truncation to int in various places). (CVE-2026-44927) + (Sebastian Pipping) + . Fixed CVE-2026-44928 (In uriparser before 1.0.2, the function family + EqualsUri can misclassify two unequal URIs as equal). (CVE-2026-44928) + (Sebastian Pipping) + 07 May 2026, PHP 8.5.6 - Core: diff --git a/ext/uri/uriparser/include/uriparser/Uri.h b/ext/uri/uriparser/include/uriparser/Uri.h index f041f8113531..ddbbd26f9e44 100644 --- a/ext/uri/uriparser/include/uriparser/Uri.h +++ b/ext/uri/uriparser/include/uriparser/Uri.h @@ -1,4 +1,4 @@ -/* 53c1cb9f2f728652fe001dc72fa0fa7a0e9fa0b8baaaa9e37561c6cdf88ac4df (1.0.1+) +/* c9d94656d067288e474df19a062d487c736b0fa8517d2ef7bbeb8dcd5a70c05b (1.0.2+) * * uriparser - RFC 3986 URI parsing library * diff --git a/ext/uri/uriparser/include/uriparser/UriBase.h b/ext/uri/uriparser/include/uriparser/UriBase.h index abadcae0dbdc..1cc91ceab772 100644 --- a/ext/uri/uriparser/include/uriparser/UriBase.h +++ b/ext/uri/uriparser/include/uriparser/UriBase.h @@ -52,7 +52,7 @@ /* Version */ # define URI_VER_MAJOR 1 # define URI_VER_MINOR 0 -# define URI_VER_RELEASE 1 +# define URI_VER_RELEASE 2 # define URI_VER_SUFFIX_ANSI "" # define URI_VER_SUFFIX_UNICODE URI_ANSI_TO_UNICODE(URI_VER_SUFFIX_ANSI) diff --git a/ext/uri/uriparser/src/UriCommon.c b/ext/uri/uriparser/src/UriCommon.c index 00256f201f7e..e9a6992b01c2 100644 --- a/ext/uri/uriparser/src/UriCommon.c +++ b/ext/uri/uriparser/src/UriCommon.c @@ -67,6 +67,7 @@ # include # include +# include // SIZE_MAX /*extern*/ const URI_CHAR * const URI_FUNC(SafeToPointTo) = _UT("X"); /*extern*/ const URI_CHAR * const URI_FUNC(ConstPwd) = _UT("."); @@ -105,46 +106,35 @@ int URI_FUNC(FreeUriPath)(URI_TYPE(Uri) * uri, UriMemoryManager * memory) { } /* Compares two text ranges for equal text content */ -int URI_FUNC(CompareRange)(const URI_TYPE(TextRange) * a, const URI_TYPE(TextRange) * b) { - int diff; - ptrdiff_t lenA; - ptrdiff_t lenB; - +bool URI_FUNC(RangeEquals)(const URI_TYPE(TextRange) * a, const URI_TYPE(TextRange) * b) { /* NOTE: Both NULL means equal! */ if ((a == NULL) || (b == NULL)) { - return ((a == NULL) ? 0 : 1) - ((b == NULL) ? 0 : 1); + return a == b; } /* NOTE: Both NULL means equal! */ if ((a->first == NULL) || (b->first == NULL)) { - return ((a->first == NULL) ? 0 : 1) - ((b->first == NULL) ? 0 : 1); - } - - lenA = a->afterLast - a->first; - lenB = b->afterLast - b->first; - - if (lenA > lenB) { - return 1; - } else if (lenA < lenB) { - return -1; + return a->first == b->first; } - diff = URI_STRNCMP(a->first, b->first, (size_t)lenA); + const size_t lenA = a->afterLast - a->first; + const size_t lenB = b->afterLast - b->first; - if (diff > 0) { - return 1; - } else if (diff < 0) { - return -1; + if (lenA != lenB) { + return false; } - return diff; + return URI_STRNCMP(a->first, b->first, lenA) == 0; } UriBool URI_FUNC(CopyRange)(URI_TYPE(TextRange) * destRange, const URI_TYPE(TextRange) * sourceRange, UriMemoryManager * memory) { - const int lenInChars = (int)(sourceRange->afterLast - sourceRange->first); - const int lenInBytes = lenInChars * sizeof(URI_CHAR); + const size_t lenInChars = sourceRange->afterLast - sourceRange->first; + if (lenInChars > SIZE_MAX / sizeof(URI_CHAR)) { // detect integer overflow + return URI_FALSE; + } + const size_t lenInBytes = lenInChars * sizeof(URI_CHAR); URI_CHAR * dup = memory->malloc(memory, lenInBytes); if (dup == NULL) { return URI_FALSE; @@ -183,7 +173,7 @@ UriBool URI_FUNC(RemoveDotSegmentsEx)(URI_TYPE(Uri) * uri, UriBool relative, walker->reserved = NULL; /* Prev pointer */ do { UriBool removeSegment = URI_FALSE; - int len = (int)(walker->text.afterLast - walker->text.first); + const size_t len = walker->text.afterLast - walker->text.first; switch (len) { case 1: if ((walker->text.first)[0] == _UT('.')) { diff --git a/ext/uri/uriparser/src/UriCommon.h b/ext/uri/uriparser/src/UriCommon.h index d141935f19e5..12c691a73386 100644 --- a/ext/uri/uriparser/src/UriCommon.h +++ b/ext/uri/uriparser/src/UriCommon.h @@ -67,6 +67,8 @@ # include # endif +# include + /* Used to point to from empty path segments. * X.first and X.afterLast must be the same non-NULL value then. */ extern const URI_CHAR * const URI_FUNC(SafeToPointTo); @@ -77,7 +79,7 @@ void URI_FUNC(ResetUri)(URI_TYPE(Uri) * uri); int URI_FUNC(FreeUriPath)(URI_TYPE(Uri) * uri, UriMemoryManager * memory); -int URI_FUNC(CompareRange)(const URI_TYPE(TextRange) * a, const URI_TYPE(TextRange) * b); +bool URI_FUNC(RangeEquals)(const URI_TYPE(TextRange) * a, const URI_TYPE(TextRange) * b); UriBool URI_FUNC(CopyRange)(URI_TYPE(TextRange) * destRange, const URI_TYPE(TextRange) * sourceRange, diff --git a/ext/uri/uriparser/src/UriCompare.c b/ext/uri/uriparser/src/UriCompare.c index 781100e7e6f4..f9def86f6669 100644 --- a/ext/uri/uriparser/src/UriCompare.c +++ b/ext/uri/uriparser/src/UriCompare.c @@ -72,17 +72,17 @@ UriBool URI_FUNC(EqualsUri)(const URI_TYPE(Uri) * a, const URI_TYPE(Uri) * b) { } /* scheme */ - if (URI_FUNC(CompareRange)(&(a->scheme), &(b->scheme))) { + if (!URI_FUNC(RangeEquals)(&(a->scheme), &(b->scheme))) { return URI_FALSE; } - /* absolutePath */ - if ((a->scheme.first == NULL) && (a->absolutePath != b->absolutePath)) { + /* absolutePath -- not meaningful for URIs with a host set! */ + if (!URI_FUNC(HasHost)(a) && (a->absolutePath != b->absolutePath)) { return URI_FALSE; } /* userInfo */ - if (URI_FUNC(CompareRange)(&(a->userInfo), &(b->userInfo))) { + if (!URI_FUNC(RangeEquals)(&(a->userInfo), &(b->userInfo))) { return URI_FALSE; } @@ -107,20 +107,20 @@ UriBool URI_FUNC(EqualsUri)(const URI_TYPE(Uri) * a, const URI_TYPE(Uri) * b) { } if (a->hostData.ipFuture.first != NULL) { - if (URI_FUNC(CompareRange)(&(a->hostData.ipFuture), &(b->hostData.ipFuture))) { + if (!URI_FUNC(RangeEquals)(&(a->hostData.ipFuture), &(b->hostData.ipFuture))) { return URI_FALSE; } } if ((a->hostData.ip4 == NULL) && (a->hostData.ip6 == NULL) && (a->hostData.ipFuture.first == NULL)) { - if (URI_FUNC(CompareRange)(&(a->hostText), &(b->hostText))) { + if (!URI_FUNC(RangeEquals)(&(a->hostText), &(b->hostText))) { return URI_FALSE; } } /* portText */ - if (URI_FUNC(CompareRange)(&(a->portText), &(b->portText))) { + if (!URI_FUNC(RangeEquals)(&(a->portText), &(b->portText))) { return URI_FALSE; } @@ -133,7 +133,7 @@ UriBool URI_FUNC(EqualsUri)(const URI_TYPE(Uri) * a, const URI_TYPE(Uri) * b) { URI_TYPE(PathSegment) * walkA = a->pathHead; URI_TYPE(PathSegment) * walkB = b->pathHead; do { - if (URI_FUNC(CompareRange)(&(walkA->text), &(walkB->text))) { + if (!URI_FUNC(RangeEquals)(&(walkA->text), &(walkB->text))) { return URI_FALSE; } if ((walkA->next == NULL) != (walkB->next == NULL)) { @@ -145,12 +145,12 @@ UriBool URI_FUNC(EqualsUri)(const URI_TYPE(Uri) * a, const URI_TYPE(Uri) * b) { } /* query */ - if (URI_FUNC(CompareRange)(&(a->query), &(b->query))) { + if (!URI_FUNC(RangeEquals)(&(a->query), &(b->query))) { return URI_FALSE; } /* fragment */ - if (URI_FUNC(CompareRange)(&(a->fragment), &(b->fragment))) { + if (!URI_FUNC(RangeEquals)(&(a->fragment), &(b->fragment))) { return URI_FALSE; } diff --git a/ext/uri/uriparser/src/UriCopy.c b/ext/uri/uriparser/src/UriCopy.c index 3bc6b0dbe3f4..fbe6340faa42 100644 --- a/ext/uri/uriparser/src/UriCopy.c +++ b/ext/uri/uriparser/src/UriCopy.c @@ -199,6 +199,12 @@ int URI_FUNC(CopyUriMm)(URI_TYPE(Uri) * destUri, const URI_TYPE(Uri) * sourceUri if (URI_FUNC(CopyRangeAsNeeded)(&destWalker->text, &sourceWalker->text, memory) == URI_FALSE) { + // Unless wired to `destUri` above, `destWalker` may be hanging + // in the air now + if (destUri->pathHead != destWalker) { + memory->free(memory, destWalker); + } + URI_FUNC(PreventLeakageAfterCopy)(destUri, revertMask, memory); return URI_ERROR_MALLOC; } diff --git a/ext/uri/uriparser/src/UriFile.c b/ext/uri/uriparser/src/UriFile.c index 7510e701c9f5..0af88480b0ad 100644 --- a/ext/uri/uriparser/src/UriFile.c +++ b/ext/uri/uriparser/src/UriFile.c @@ -63,7 +63,8 @@ # include # endif -# include /* for size_t, avoiding stddef.h for older MSVCs */ +# include // size_t +# include // SIZE_MAX static URI_INLINE int URI_FUNC(FilenameToUriString)(const URI_CHAR * filename, URI_CHAR * uriString, @@ -90,6 +91,11 @@ static URI_INLINE int URI_FUNC(FilenameToUriString)(const URI_CHAR * filename, : _UT("file:///"); const size_t prefixLen = URI_STRLEN(prefix); + // Detect and avoid integer overflow + if (prefixLen > SIZE_MAX / sizeof(URI_CHAR)) { + return URI_ERROR_OUTPUT_TOO_LARGE; + } + /* Copy prefix */ memcpy(uriString, prefix, prefixLen * sizeof(URI_CHAR)); output += prefixLen; @@ -103,7 +109,13 @@ static URI_INLINE int URI_FUNC(FilenameToUriString)(const URI_CHAR * filename, if (lastSep + 1 < input) { if (!fromUnix && absolute && (firstSegment == URI_TRUE)) { /* Quick hack to not convert "C:" to "C%3A" */ - const int charsToCopy = (int)(input - (lastSep + 1)); + const size_t charsToCopy = input - (lastSep + 1); + + // Detect and avoid integer overflow + if (charsToCopy > SIZE_MAX / sizeof(URI_CHAR)) { + return URI_ERROR_OUTPUT_TOO_LARGE; + } + memcpy(output, lastSep + 1, charsToCopy * sizeof(URI_CHAR)); output += charsToCopy; } else { diff --git a/ext/uri/uriparser/src/UriNormalize.c b/ext/uri/uriparser/src/UriNormalize.c index 8c812d37a0a7..c73e22b28879 100644 --- a/ext/uri/uriparser/src/UriNormalize.c +++ b/ext/uri/uriparser/src/UriNormalize.c @@ -73,6 +73,7 @@ # endif # include +# include // SIZE_MAX static int URI_FUNC(NormalizeSyntaxEngine)(URI_TYPE(Uri) * uri, unsigned int inMask, unsigned int * outMask, @@ -254,20 +255,22 @@ URI_FUNC(LowercaseInplaceExceptPercentEncoding)(const URI_CHAR * first, static URI_INLINE UriBool URI_FUNC(LowercaseMalloc)(const URI_CHAR ** first, const URI_CHAR ** afterLast, UriMemoryManager * memory) { - int lenInChars; const int lowerUpperDiff = (_UT('a') - _UT('A')); URI_CHAR * buffer; - int i = 0; + size_t i = 0; if ((first == NULL) || (afterLast == NULL) || (*first == NULL) || (*afterLast == NULL)) { return URI_FALSE; } - lenInChars = (int)(*afterLast - *first); + const size_t lenInChars = *afterLast - *first; if (lenInChars == 0) { return URI_TRUE; - } else if (lenInChars < 0) { + } + + // Detect and avoid integer overflow + if (lenInChars > SIZE_MAX / sizeof(URI_CHAR)) { return URI_FALSE; } @@ -295,8 +298,8 @@ URI_FUNC(FixPercentEncodingEngine)(const URI_CHAR * inFirst, const URI_CHAR * in const URI_CHAR * outFirst, const URI_CHAR ** outAfterLast) { URI_CHAR * write = (URI_CHAR *)outFirst; - const int lenInChars = (int)(inAfterLast - inFirst); - int i = 0; + const size_t lenInChars = inAfterLast - inFirst; + size_t i = 0; /* All but last two */ for (; i + 2 < lenInChars; i++) { @@ -350,7 +353,6 @@ static URI_INLINE void URI_FUNC(FixPercentEncodingInplace)(const URI_CHAR * firs static URI_INLINE UriBool URI_FUNC(FixPercentEncodingMalloc)(const URI_CHAR ** first, const URI_CHAR ** afterLast, UriMemoryManager * memory) { - int lenInChars; URI_CHAR * buffer; /* Death checks */ @@ -360,10 +362,13 @@ static URI_INLINE UriBool URI_FUNC(FixPercentEncodingMalloc)(const URI_CHAR ** f } /* Old text length */ - lenInChars = (int)(*afterLast - *first); + const size_t lenInChars = *afterLast - *first; if (lenInChars == 0) { return URI_TRUE; - } else if (lenInChars < 0) { + } + + // Detect and avoid integer overflow + if (lenInChars > SIZE_MAX / sizeof(URI_CHAR)) { return URI_FALSE; } diff --git a/ext/uri/uriparser/src/UriQuery.c b/ext/uri/uriparser/src/UriQuery.c index 801a237a3a1b..de3a040de042 100644 --- a/ext/uri/uriparser/src/UriQuery.c +++ b/ext/uri/uriparser/src/UriQuery.c @@ -67,6 +67,7 @@ # include # include /* size_t */ +# include // SIZE_MAX static int URI_FUNC(ComposeQueryEngine)(URI_CHAR * dest, const URI_TYPE(QueryList) * queryList, @@ -254,7 +255,14 @@ int URI_FUNC(ComposeQueryEngine)(URI_CHAR * dest, const URI_TYPE(QueryList) * qu if (dest != NULL) { write[0] = _UT('\0'); if (charsWritten != NULL) { - *charsWritten = (int)(write - dest) + 1; /* .. for terminator */ + const size_t lenInChars = write - dest; + + // Detect and avoid integer overflow + if (lenInChars > INT_MAX - 1) { + return URI_ERROR_OUTPUT_TOO_LARGE; + } + + *charsWritten = (int)(lenInChars + 1); /* .. for terminator */ } } @@ -267,8 +275,8 @@ UriBool URI_FUNC(AppendQueryItem)(URI_TYPE(QueryList) * *prevNext, int * itemCou const URI_CHAR * valueAfter, UriBool plusToSpace, UriBreakConversion breakConversion, UriMemoryManager * memory) { - const int keyLen = (int)(keyAfter - keyFirst); - const int valueLen = (int)(valueAfter - valueFirst); + const size_t keyLen = keyAfter - keyFirst; + const size_t valueLen = valueAfter - valueFirst; URI_CHAR * key; URI_CHAR * value; @@ -285,6 +293,13 @@ UriBool URI_FUNC(AppendQueryItem)(URI_TYPE(QueryList) * *prevNext, int * itemCou } (*prevNext)->next = NULL; + // Detect integer overflow + if ((keyLen > SIZE_MAX - 1) || (keyLen + 1 > SIZE_MAX / sizeof(URI_CHAR))) { + memory->free(memory, *prevNext); + *prevNext = NULL; + return URI_FALSE; // Raises malloc error + } + /* Fill key */ key = memory->malloc(memory, (keyLen + 1) * sizeof(URI_CHAR)); if (key == NULL) { @@ -305,6 +320,14 @@ UriBool URI_FUNC(AppendQueryItem)(URI_TYPE(QueryList) * *prevNext, int * itemCou /* Fill value */ if (valueFirst != NULL) { + // Detect integer overflow + if ((valueLen > SIZE_MAX - 1) || (valueLen + 1 > SIZE_MAX / sizeof(URI_CHAR))) { + memory->free(memory, key); + memory->free(memory, *prevNext); + *prevNext = NULL; + return URI_FALSE; // Raises malloc error + } + value = memory->malloc(memory, (valueLen + 1) * sizeof(URI_CHAR)); if (value == NULL) { memory->free(memory, key); diff --git a/ext/uri/uriparser/src/UriRecompose.c b/ext/uri/uriparser/src/UriRecompose.c index 61ffee77248f..055e0483d3f2 100644 --- a/ext/uri/uriparser/src/UriRecompose.c +++ b/ext/uri/uriparser/src/UriRecompose.c @@ -64,6 +64,9 @@ # include "UriCommon.h" # endif +# include // INT_MAX +# include // SIZE_MAX + static int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(Uri) * uri, int maxChars, int * charsWritten, int * charsRequired); @@ -116,10 +119,19 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U /* clang-format off */ /* [03/19] append scheme to result; */ /* clang-format on */ - const int charsToWrite = - (int)(uri->scheme.afterLast - uri->scheme.first); + const size_t charsToWrite = uri->scheme.afterLast - uri->scheme.first; if (dest != NULL) { - if (written + charsToWrite <= maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > (size_t)INT_MAX - written) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + + if (written + charsToWrite <= (size_t)maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > SIZE_MAX / sizeof(URI_CHAR)) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + memcpy(dest + written, uri->scheme.first, charsToWrite * sizeof(URI_CHAR)); written += charsToWrite; @@ -131,6 +143,11 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U return URI_ERROR_TOSTRING_TOO_LONG; } } else { + // Detect and avoid integer overflow + if (charsToWrite > (size_t)INT_MAX - *charsRequired) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + (*charsRequired) += charsToWrite; } /* clang-format off */ @@ -180,10 +197,20 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U /* clang-format on */ /* UserInfo */ if (uri->userInfo.first != NULL) { - const int charsToWrite = - (int)(uri->userInfo.afterLast - uri->userInfo.first); + const size_t charsToWrite = + uri->userInfo.afterLast - uri->userInfo.first; if (dest != NULL) { - if (written + charsToWrite <= maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > (size_t)INT_MAX - written) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + + if (written + charsToWrite <= (size_t)maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > SIZE_MAX / sizeof(URI_CHAR)) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + memcpy(dest + written, uri->userInfo.first, charsToWrite * sizeof(URI_CHAR)); written += charsToWrite; @@ -206,6 +233,13 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U return URI_ERROR_TOSTRING_TOO_LONG; } } else { + // Detect and avoid integer overflow + if ((charsToWrite > (size_t)INT_MAX - 1) + || (charsToWrite + 1 + > (size_t)INT_MAX - *charsRequired)) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + (*charsRequired) += charsToWrite + 1; } } @@ -334,8 +368,8 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U } } else if (uri->hostData.ipFuture.first != NULL) { /* IPvFuture */ - const int charsToWrite = (int)(uri->hostData.ipFuture.afterLast - - uri->hostData.ipFuture.first); + const size_t charsToWrite = uri->hostData.ipFuture.afterLast + - uri->hostData.ipFuture.first; if (dest != NULL) { if (written + 1 <= maxChars) { memcpy(dest + written, _UT("["), 1 * sizeof(URI_CHAR)); @@ -348,7 +382,17 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U return URI_ERROR_TOSTRING_TOO_LONG; } - if (written + charsToWrite <= maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > (size_t)INT_MAX - written) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + + if (written + charsToWrite <= (size_t)maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > SIZE_MAX / sizeof(URI_CHAR)) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + memcpy(dest + written, uri->hostData.ipFuture.first, charsToWrite * sizeof(URI_CHAR)); written += charsToWrite; @@ -371,14 +415,31 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U return URI_ERROR_TOSTRING_TOO_LONG; } } else { + // Detect and avoid integer overflow + if ((charsToWrite > (size_t)INT_MAX - 1 - 1) + || (1 + charsToWrite + 1 + > (size_t)INT_MAX - *charsRequired)) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + (*charsRequired) += 1 + charsToWrite + 1; } } else if (uri->hostText.first != NULL) { /* Regname */ - const int charsToWrite = - (int)(uri->hostText.afterLast - uri->hostText.first); + const size_t charsToWrite = + uri->hostText.afterLast - uri->hostText.first; if (dest != NULL) { - if (written + charsToWrite <= maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > (size_t)INT_MAX - written) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + + if (written + charsToWrite <= (size_t)maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > SIZE_MAX / sizeof(URI_CHAR)) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + memcpy(dest + written, uri->hostText.first, charsToWrite * sizeof(URI_CHAR)); written += charsToWrite; @@ -390,14 +451,19 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U return URI_ERROR_TOSTRING_TOO_LONG; } } else { + // Detect and avoid integer overflow + if (charsToWrite > (size_t)INT_MAX - *charsRequired) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + (*charsRequired) += charsToWrite; } } /* Port */ if (uri->portText.first != NULL) { - const int charsToWrite = - (int)(uri->portText.afterLast - uri->portText.first); + const size_t charsToWrite = + uri->portText.afterLast - uri->portText.first; if (dest != NULL) { /* Leading ':' */ if (written + 1 <= maxChars) { @@ -411,8 +477,18 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U return URI_ERROR_TOSTRING_TOO_LONG; } + // Detect and avoid integer overflow + if (charsToWrite > (size_t)INT_MAX - written) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + /* Port number */ - if (written + charsToWrite <= maxChars) { + if (written + charsToWrite <= (size_t)maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > SIZE_MAX / sizeof(URI_CHAR)) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + memcpy(dest + written, uri->portText.first, charsToWrite * sizeof(URI_CHAR)); written += charsToWrite; @@ -424,6 +500,13 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U return URI_ERROR_TOSTRING_TOO_LONG; } } else { + // Detect and avoid integer overflow + if ((charsToWrite > (size_t)INT_MAX - 1) + || (1 + charsToWrite + > (size_t)INT_MAX - *charsRequired)) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + (*charsRequired) += 1 + charsToWrite; } } @@ -456,10 +539,20 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U if (uri->pathHead != NULL) { URI_TYPE(PathSegment) * walker = uri->pathHead; do { - const int charsToWrite = - (int)(walker->text.afterLast - walker->text.first); + const size_t charsToWrite = + walker->text.afterLast - walker->text.first; if (dest != NULL) { - if (written + charsToWrite <= maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > (size_t)INT_MAX - written) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + + if (written + charsToWrite <= (size_t)maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > SIZE_MAX / sizeof(URI_CHAR)) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + memcpy(dest + written, walker->text.first, charsToWrite * sizeof(URI_CHAR)); written += charsToWrite; @@ -471,6 +564,11 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U return URI_ERROR_TOSTRING_TOO_LONG; } } else { + // Detect and avoid integer overflow + if (charsToWrite > (size_t)INT_MAX - *charsRequired) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + (*charsRequired) += charsToWrite; } @@ -520,10 +618,19 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U /* clang-format off */ /* [13/19] append query to result; */ /* clang-format on */ - const int charsToWrite = - (int)(uri->query.afterLast - uri->query.first); + const size_t charsToWrite = uri->query.afterLast - uri->query.first; if (dest != NULL) { - if (written + charsToWrite <= maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > (size_t)INT_MAX - written) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + + if (written + charsToWrite <= (size_t)maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > SIZE_MAX / sizeof(URI_CHAR)) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + memcpy(dest + written, uri->query.first, charsToWrite * sizeof(URI_CHAR)); written += charsToWrite; @@ -535,6 +642,11 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U return URI_ERROR_TOSTRING_TOO_LONG; } } else { + // Detect and avoid integer overflow + if (charsToWrite > (size_t)INT_MAX - *charsRequired) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + (*charsRequired) += charsToWrite; } /* clang-format off */ @@ -565,10 +677,20 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U /* clang-format off */ /* [17/19] append fragment to result; */ /* clang-format on */ - const int charsToWrite = - (int)(uri->fragment.afterLast - uri->fragment.first); + const size_t charsToWrite = + uri->fragment.afterLast - uri->fragment.first; if (dest != NULL) { - if (written + charsToWrite <= maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > (size_t)INT_MAX - written) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + + if (written + charsToWrite <= (size_t)maxChars) { + // Detect and avoid integer overflow + if (charsToWrite > SIZE_MAX / sizeof(URI_CHAR)) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + memcpy(dest + written, uri->fragment.first, charsToWrite * sizeof(URI_CHAR)); written += charsToWrite; @@ -580,6 +702,11 @@ static URI_INLINE int URI_FUNC(ToStringEngine)(URI_CHAR * dest, const URI_TYPE(U return URI_ERROR_TOSTRING_TOO_LONG; } } else { + // Detect and avoid integer overflow + if (charsToWrite > (size_t)INT_MAX - *charsRequired) { + return URI_ERROR_TOSTRING_TOO_LONG; + } + (*charsRequired) += charsToWrite; } /* clang-format off */ diff --git a/ext/uri/uriparser/src/UriResolve.c b/ext/uri/uriparser/src/UriResolve.c index 302665d21cd6..90c8bfd778dc 100644 --- a/ext/uri/uriparser/src/UriResolve.c +++ b/ext/uri/uriparser/src/UriResolve.c @@ -183,9 +183,8 @@ static int URI_FUNC(AddBaseUriImpl)(URI_TYPE(Uri) * absDest, if ((options & URI_RESOLVE_IDENTICAL_SCHEME_COMPAT) && (absBase->scheme.first != NULL) && (relSource->scheme.first != NULL) - && (0 - == URI_FUNC(CompareRange)(&(absBase->scheme), - &(relSource->scheme)))) { + && (URI_FUNC(RangeEquals)(&(absBase->scheme), + &(relSource->scheme)))) { /* clang-format off */ /* [00/32] undefine(R.scheme); */ /* clang-format on */ diff --git a/ext/uri/uriparser/src/UriShorten.c b/ext/uri/uriparser/src/UriShorten.c index 548b0b4157dd..001bd7db9990 100644 --- a/ext/uri/uriparser/src/UriShorten.c +++ b/ext/uri/uriparser/src/UriShorten.c @@ -111,14 +111,14 @@ static URI_INLINE UriBool URI_FUNC(EqualsAuthority)(const URI_TYPE(Uri) * first, /* IPvFuture */ if (first->hostData.ipFuture.first != NULL) { return ((second->hostData.ipFuture.first != NULL) - && !URI_FUNC(CompareRange)(&first->hostData.ipFuture, - &second->hostData.ipFuture)) + && URI_FUNC(RangeEquals)(&first->hostData.ipFuture, + &second->hostData.ipFuture)) ? URI_TRUE : URI_FALSE; } - return !URI_FUNC(CompareRange)(&first->hostText, &second->hostText) ? URI_TRUE - : URI_FALSE; + return URI_FUNC(RangeEquals)(&first->hostText, &second->hostText) ? URI_TRUE + : URI_FALSE; } static int URI_FUNC(RemoveBaseUriImpl)(URI_TYPE(Uri) * dest, @@ -152,7 +152,7 @@ static int URI_FUNC(RemoveBaseUriImpl)(URI_TYPE(Uri) * dest, /* clang-format off */ /* [01/50] if (A.scheme != Base.scheme) then */ /* clang-format on */ - if (URI_FUNC(CompareRange)(&absSource->scheme, &absBase->scheme)) { + if (!URI_FUNC(RangeEquals)(&absSource->scheme, &absBase->scheme)) { /* clang-format off */ /* [02/50] T.scheme = A.scheme; */ /* clang-format on */ @@ -255,8 +255,7 @@ static int URI_FUNC(RemoveBaseUriImpl)(URI_TYPE(Uri) * dest, /* clang-format on */ while ( (sourceSeg != NULL) && (baseSeg != NULL) - && !URI_FUNC(CompareRange)(&sourceSeg->text, - &baseSeg->text) + && URI_FUNC(RangeEquals)(&sourceSeg->text, &baseSeg->text) && !((sourceSeg->text.first == sourceSeg->text.afterLast) && ((sourceSeg->next == NULL) != (baseSeg->next == NULL)))) { From 2c32bdf1d45ed0d0bdabfa948314350c5570b31b Mon Sep 17 00:00:00 2001 From: Shivam Mathur Date: Mon, 18 May 2026 20:51:41 +0530 Subject: [PATCH 3/6] Fix GH-22023 crash during ZTS thread startup (#22027) --- Zend/zend.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Zend/zend.c b/Zend/zend.c index 045d25134f8c..59a1a1a3ba88 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -809,6 +809,7 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{ executor_globals->user_error_handler_error_reporting = 0; ZVAL_UNDEF(&executor_globals->user_error_handler); ZVAL_UNDEF(&executor_globals->user_exception_handler); + ZVAL_UNDEF(&executor_globals->last_fatal_error_backtrace); executor_globals->in_autoload = NULL; executor_globals->current_execute_data = NULL; executor_globals->current_module = NULL; From 06bfe158a52fb79077e46328f4c1a44f0de4e8b0 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Thu, 30 Apr 2026 10:07:12 -0600 Subject: [PATCH 4/6] Fix tailcall helper dispatch after VM interrupt In the tailcall VM, helpers which have extra args return the next opline instead of tailcalling it, relying on the parent to call it in ZEND_VM_DISPATCH_TO_HELPER(). When an interrupt is handled in the helper the opline may be tagged with ZEND_VM_ENTER_BIT, but ZEND_VM_DISPATCH_TO_HELPER() assumes an untagged opline. This PR changes ZEND_VM_INTERRUPT() so that such helpers return an opline whose handler is zend_interrupt_helper, instead of executing it directly. Closes GH-21922 Co-Authored-By: Arnaud Le Blanc --- NEWS | 2 + Zend/zend_vm.h | 5 ++ Zend/zend_vm_def.h | 5 ++ Zend/zend_vm_execute.h | 50 ++++++++++++++++++- Zend/zend_vm_execute.skl | 16 ++++++ Zend/zend_vm_gen.php | 31 ++++++++++-- ext/opcache/jit/zend_jit.c | 2 + ext/opcache/jit/zend_jit_internal.h | 1 + ext/opcache/jit/zend_jit_vm_helpers.c | 5 ++ ext/zend_test/object_handlers.c | 43 ++++++++++++++++ ext/zend_test/object_handlers.stub.php | 5 ++ ext/zend_test/object_handlers_arginfo.h | 26 +++++++++- ...observer_vm_interrupt_tailcall_helper.phpt | 26 ++++++++++ 13 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 ext/zend_test/tests/observer_vm_interrupt_tailcall_helper.phpt diff --git a/NEWS b/NEWS index 7d4f5cde9fec..0abc46c59075 100644 --- a/NEWS +++ b/NEWS @@ -15,6 +15,8 @@ PHP NEWS . Fixed bug GH-21746 (Segfault with tracing JIT). (Arnaud) . Fixed bug GH-22004 (Assertion failure at ext/opcache/jit/zend_jit_trace.c). (Arnaud) + . Fixed tailcall VM crash when a VM interrupt is handled from a VM helper. + (Levi Morrison, Arnaud) - OpenSSL: . Fix compatibility issues with OpenSSL 4.0. (jordikroon, Remi) diff --git a/Zend/zend_vm.h b/Zend/zend_vm.h index c9c41c75c72a..0727fdb15c76 100644 --- a/Zend/zend_vm.h +++ b/Zend/zend_vm.h @@ -32,6 +32,7 @@ ZEND_API void ZEND_FASTCALL zend_serialize_opcode_handler(zend_op *op); ZEND_API void ZEND_FASTCALL zend_deserialize_opcode_handler(zend_op *op); ZEND_API const void* ZEND_FASTCALL zend_get_opcode_handler_func(const zend_op *op); ZEND_API const zend_op *zend_get_halt_op(void); +ZEND_API const zend_op *zend_get_interrupt_op(void); ZEND_API int ZEND_FASTCALL zend_vm_call_opcode_handler(zend_execute_data *ex); ZEND_API int zend_vm_kind(void); ZEND_API bool zend_gcc_global_regs(void); @@ -39,6 +40,10 @@ ZEND_API bool zend_gcc_global_regs(void); void zend_vm_init(void); void zend_vm_dtor(void); +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL +const struct _zend_op *zend_vm_handle_interrupt(struct _zend_execute_data *execute_data, const struct _zend_op *opline); +#endif + END_EXTERN_C() #define ZEND_VM_SET_OPCODE_HANDLER(opline) zend_vm_set_opcode_handler(opline) diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 6712625e83b1..fdfe48fc8421 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -10523,7 +10523,12 @@ ZEND_VM_DEFINE_OP(137, ZEND_OP_DATA); ZEND_VM_HELPER(zend_interrupt_helper, ANY, ANY) { zend_atomic_bool_store_ex(&EG(vm_interrupt), false); +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL + /* opline is &call_interrupt_op. Load orig opline. */ + LOAD_OPLINE(); +#else SAVE_OPLINE(); +#endif if (zend_atomic_bool_load_ex(&EG(timed_out))) { zend_timeout(); } else if (zend_interrupt_function) { diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 0d11a17ce781..0faa84444be3 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -331,6 +331,11 @@ static zend_op hybrid_halt_op; static zend_vm_opcode_handler_func_t const * zend_opcode_handler_funcs; #endif + +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL +static const zend_op call_interrupt_op; +#endif + #if (ZEND_VM_KIND != ZEND_VM_KIND_HYBRID && ZEND_VM_KIND != ZEND_VM_KIND_TAILCALL) || !ZEND_VM_SPEC static zend_vm_opcode_handler_t zend_vm_get_opcode_handler(uint8_t opcode, const zend_op* op); #endif @@ -4011,7 +4016,12 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_J static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS) { zend_atomic_bool_store_ex(&EG(vm_interrupt), false); +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL + /* opline is &call_interrupt_op. Load orig opline. */ + LOAD_OPLINE(); +#else SAVE_OPLINE(); +#endif if (zend_atomic_bool_load_ex(&EG(timed_out))) { zend_timeout(); } else if (zend_interrupt_function) { @@ -56498,6 +56508,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NULL_HANDLER( ZEND_VM_NEXT_OPCODE(); /* Never reached */ } +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL +static ZEND_COLD zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_interrupt(ZEND_OPCODE_HANDLER_ARGS) { + SAVE_OPLINE(); + return &call_interrupt_op; +} +#endif #if (ZEND_VM_KIND == ZEND_VM_KIND_HYBRID) # undef ZEND_VM_TAIL_CALL @@ -56526,9 +56542,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NULL_HANDLER( ZEND_VM_TAIL_CALL(opline->handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); \ } while (0) # define ZEND_VM_DISPATCH_TO_LEAVE_HELPER(helper) opline = &call_leave_op; SAVE_OPLINE(); ZEND_VM_CONTINUE() -# define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) +# define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS); +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_interrupt(ZEND_OPCODE_HANDLER_ARGS); +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS); static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NULL_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS); static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_HALT_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS); static zend_never_inline const zend_op *ZEND_OPCODE_HANDLER_CCONV zend_leave_helper_SPEC_TAILCALL(zend_execute_data *ex, const zend_op *opline); @@ -56539,6 +56557,9 @@ static const zend_op call_halt_op = { static const zend_op call_leave_op = { .handler = zend_leave_helper_SPEC_TAILCALL, }; +static const zend_op call_interrupt_op = { + .handler = zend_interrupt_helper_SPEC_TAILCALL, +}; static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV_EX zend_add_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_EX zval *op_1, zval *op_2); static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV_EX zend_sub_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_EX zval *op_1, zval *op_2); @@ -59674,7 +59695,12 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_JMP_FO static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS) { zend_atomic_bool_store_ex(&EG(vm_interrupt), false); +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL + /* opline is &call_interrupt_op. Load orig opline. */ + LOAD_OPLINE(); +#else SAVE_OPLINE(); +#endif if (zend_atomic_bool_load_ex(&EG(timed_out))) { zend_timeout(); } else if (zend_interrupt_function) { @@ -111862,13 +111888,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_HALT_TAILCALL_HAND return (zend_op*) ZEND_VM_ENTER_BIT; } +static ZEND_COLD zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS) { + SAVE_OPLINE(); + ZEND_VM_TAIL_CALL(zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); +} /* The following helpers can not tailcall due to signature mismatch. Redefine some macros so they do not enforce tailcall. */ #pragma push_macro("ZEND_VM_CONTINUE") #undef ZEND_VM_CONTINUE #pragma push_macro("ZEND_VM_INTERRUPT") #undef ZEND_VM_INTERRUPT #define ZEND_VM_CONTINUE(handler) return opline -#define ZEND_VM_INTERRUPT() return zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU) +#define ZEND_VM_INTERRUPT() return zend_interrupt(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU) static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV_EX zend_add_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_EX zval *op_1, zval *op_2) { USE_OPLINE @@ -129262,6 +129292,22 @@ ZEND_API const zend_op *zend_get_halt_op(void) #endif } +ZEND_API const zend_op *zend_get_interrupt_op(void) +{ +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL + return &call_interrupt_op; +#else + return NULL; +#endif +} + +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL +ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_vm_handle_interrupt(ZEND_OPCODE_HANDLER_ARGS) +{ + return zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); +} +#endif + ZEND_API int zend_vm_kind(void) { return ZEND_VM_KIND; diff --git a/Zend/zend_vm_execute.skl b/Zend/zend_vm_execute.skl index 53b1ac6baf0a..f7bbfc99b578 100644 --- a/Zend/zend_vm_execute.skl +++ b/Zend/zend_vm_execute.skl @@ -161,6 +161,22 @@ ZEND_API const zend_op *zend_get_halt_op(void) #endif } +ZEND_API const zend_op *zend_get_interrupt_op(void) +{ +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL + return &call_interrupt_op; +#else + return NULL; +#endif +} + +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL +ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_vm_handle_interrupt(ZEND_OPCODE_HANDLER_ARGS) +{ + return zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); +} +#endif + ZEND_API int zend_vm_kind(void) { return ZEND_VM_KIND; diff --git a/Zend/zend_vm_gen.php b/Zend/zend_vm_gen.php index 38c20b24da2e..1ffa80f718dc 100755 --- a/Zend/zend_vm_gen.php +++ b/Zend/zend_vm_gen.php @@ -1594,6 +1594,19 @@ function gen_halt_handler($f, $kind) { out($f,"}\n\n"); } +function gen_interrupt_func($f, $kind, $spec) { + $cconv = $kind === ZEND_VM_KIND_TAILCALL ? 'ZEND_OPCODE_HANDLER_CCONV' : 'ZEND_OPCODE_HANDLER_FUNC_CCONV'; + $variant = $kind === ZEND_VM_KIND_TAILCALL ? '_TAILCALL' : ''; + out($f, "static ZEND_COLD zend_never_inline ZEND_OPCODE_HANDLER_RET {$cconv} zend_interrupt{$variant}(ZEND_OPCODE_HANDLER_ARGS) {\n"); + out($f,"\tSAVE_OPLINE();\n"); + if ($kind === ZEND_VM_KIND_TAILCALL) { + out($f,"\tZEND_VM_TAIL_CALL(zend_interrupt_helper".($spec?"_SPEC":"")."_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));\n"); + } else { + out($f, "\treturn &call_interrupt_op;\n"); + } + out($f, "}\n"); +} + function extra_spec_name($extra_spec) { global $prefix; @@ -1804,10 +1817,14 @@ function gen_executor_code($f, $spec, $kind, $prolog, &$switch_labels = array()) switch ($kind) { case ZEND_VM_KIND_CALL: gen_null_handler($f, $kind); + out($f, "#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL\n"); + gen_interrupt_func($f, $kind, $spec); + out($f, "#endif\n"); break; case ZEND_VM_KIND_TAILCALL: gen_null_handler($f, $kind); gen_halt_handler($f, $kind); + gen_interrupt_func($f, $kind, $spec); break; case ZEND_VM_KIND_SWITCH: out($f,"default: ZEND_NULL_LABEL:\n"); @@ -1845,7 +1862,7 @@ function gen_executor_code($f, $spec, $kind, $prolog, &$switch_labels = array()) out($f, "#pragma push_macro(\"ZEND_VM_INTERRUPT\")\n"); out($f, "#undef ZEND_VM_INTERRUPT\n"); out($f, "#define ZEND_VM_CONTINUE(handler) return opline\n"); - out($f, "#define ZEND_VM_INTERRUPT() return zend_interrupt_helper".($spec?"_SPEC":"")."(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)\n"); + out($f, "#define ZEND_VM_INTERRUPT() return zend_interrupt(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)\n"); out($f, $delayed_helpers); out($f, "#pragma pop_macro(\"ZEND_VM_INTERRUPT\")\n"); out($f, "#pragma pop_macro(\"ZEND_VM_CONTINUE\")\n"); @@ -1900,7 +1917,10 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name) if ($kind == ZEND_VM_KIND_HYBRID || $kind == ZEND_VM_KIND_CALL) { out($f,"#if ZEND_VM_KIND == ZEND_VM_KIND_HYBRID || ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL\n\n"); out($f,"static zend_vm_opcode_handler_func_t const * zend_opcode_handler_funcs;\n"); - out($f,"#endif\n"); + out($f,"#endif\n\n"); + out($f,"#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL\n"); + out($f,"static const zend_op call_interrupt_op;\n"); + out($f,"#endif\n\n"); } out($f,"#if (ZEND_VM_KIND != ZEND_VM_KIND_HYBRID && ZEND_VM_KIND != ZEND_VM_KIND_TAILCALL) || !ZEND_VM_SPEC\n"); out($f,"static zend_vm_opcode_handler_t zend_vm_get_opcode_handler(uint8_t opcode, const zend_op* op);\n"); @@ -2139,9 +2159,11 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name) out($f," ZEND_VM_TAIL_CALL(opline->handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); \\\n"); out($f," } while (0)\n"); out($f,"# define ZEND_VM_DISPATCH_TO_LEAVE_HELPER(helper) opline = &call_leave_op; SAVE_OPLINE(); ZEND_VM_CONTINUE()\n"); - out($f,"# define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_helper".($spec?"_SPEC":"")."_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU))\n"); + out($f,"# define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU))\n"); out($f,"\n"); out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_helper".($spec?"_SPEC":"")."_TAILCALL(ZEND_OPCODE_HANDLER_ARGS);\n"); + out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_interrupt(ZEND_OPCODE_HANDLER_ARGS);\n"); + out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS);\n"); out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NULL_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS);\n"); out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_HALT_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS);\n"); out($f,"static zend_never_inline const zend_op *ZEND_OPCODE_HANDLER_CCONV zend_leave_helper_SPEC_TAILCALL(zend_execute_data *ex, const zend_op *opline);\n"); @@ -2152,6 +2174,9 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name) out($f,"static const zend_op call_leave_op = {\n"); out($f," .handler = zend_leave_helper_SPEC_TAILCALL,\n"); out($f,"};\n"); + out($f,"static const zend_op call_interrupt_op = {\n"); + out($f," .handler = zend_interrupt_helper_SPEC_TAILCALL,\n"); + out($f,"};\n"); out($f,"\n"); gen_executor_code($f, $spec, ZEND_VM_KIND_TAILCALL, $m[1]); diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index 41cb01761412..5de9c81484ba 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -78,6 +78,7 @@ int zend_jit_profile_counter_rid = -1; int16_t zend_jit_hot_counters[ZEND_HOT_COUNTERS_COUNT]; const zend_op *zend_jit_halt_op = NULL; +const zend_op *zend_jit_interrupt_op = NULL; #ifdef HAVE_PTHREAD_JIT_WRITE_PROTECT_NP static int zend_write_protect = 1; #endif @@ -3777,6 +3778,7 @@ int zend_jit_check_support(void) void zend_jit_startup(void *buf, size_t size, bool reattached) { zend_jit_halt_op = zend_get_halt_op(); + zend_jit_interrupt_op = zend_get_interrupt_op(); zend_jit_profile_counter_rid = zend_get_op_array_extension_handle(ACCELERATOR_PRODUCT_NAME); #ifdef HAVE_PTHREAD_JIT_WRITE_PROTECT_NP diff --git a/ext/opcache/jit/zend_jit_internal.h b/ext/opcache/jit/zend_jit_internal.h index 57c0dedb2fa2..8732b424f00a 100644 --- a/ext/opcache/jit/zend_jit_internal.h +++ b/ext/opcache/jit/zend_jit_internal.h @@ -177,6 +177,7 @@ typedef struct _zend_jit_op_array_hot_extension { zend_jit_hash((op_array)->opcodes) extern const zend_op *zend_jit_halt_op; +extern const zend_op *zend_jit_interrupt_op; #ifdef HAVE_GCC_GLOBAL_REGS # define EXECUTE_DATA_D void diff --git a/ext/opcache/jit/zend_jit_vm_helpers.c b/ext/opcache/jit/zend_jit_vm_helpers.c index 44f46feb5f69..77035b936885 100644 --- a/ext/opcache/jit/zend_jit_vm_helpers.c +++ b/ext/opcache/jit/zend_jit_vm_helpers.c @@ -1072,6 +1072,11 @@ zend_jit_trace_stop ZEND_FASTCALL zend_jit_trace_execute(zend_execute_data *ex, if (UNEXPECTED(opline == zend_jit_halt_op)) { #else opline = handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); +# if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL + while (UNEXPECTED(opline == zend_jit_interrupt_op)) { + opline = zend_vm_handle_interrupt(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); + } +# endif if (UNEXPECTED(((uintptr_t)opline & ~ZEND_VM_ENTER_BIT) == 0)) { #endif if (prev_opline->opcode == ZEND_YIELD || prev_opline->opcode == ZEND_YIELD_FROM) { diff --git a/ext/zend_test/object_handlers.c b/ext/zend_test/object_handlers.c index 15e362605f8e..9163c2f416a1 100644 --- a/ext/zend_test/object_handlers.c +++ b/ext/zend_test/object_handlers.c @@ -232,6 +232,44 @@ ZEND_METHOD(NumericCastableNoOperations, __construct) ZVAL_COPY(OBJ_PROP_NUM(Z_OBJ_P(ZEND_THIS), 0), n); } +static zend_class_entry *vm_interrupt_comparable_ce; +static zend_object_handlers vm_interrupt_comparable_object_handlers; + +static zend_object* vm_interrupt_comparable_object_create_ex(zend_class_entry* ce, zend_long l) { + zend_object *obj = zend_objects_new(ce); + object_properties_init(obj, ce); + obj->handlers = &vm_interrupt_comparable_object_handlers; + ZVAL_LONG(OBJ_PROP_NUM(obj, 0), l); + return obj; +} + +static zend_object *vm_interrupt_comparable_object_create(zend_class_entry *ce) +{ + return vm_interrupt_comparable_object_create_ex(ce, 0); +} + +static int vm_interrupt_comparable_compare(zval *op1, zval *op2) +{ + ZEND_COMPARE_OBJECTS_FALLBACK(op1, op2); + + zend_atomic_bool_store_ex(&EG(vm_interrupt), true); + + return ZEND_THREEWAY_COMPARE( + Z_LVAL_P(OBJ_PROP_NUM(Z_OBJ_P(op1), 0)), + Z_LVAL_P(OBJ_PROP_NUM(Z_OBJ_P(op2), 0))); +} + +ZEND_METHOD(VmInterruptComparable, __construct) +{ + zend_long l; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_LONG(l) + ZEND_PARSE_PARAMETERS_END(); + + ZVAL_LONG(OBJ_PROP_NUM(Z_OBJ_P(ZEND_THIS), 0), l); +} + static zend_class_entry *dimension_handlers_no_ArrayAccess_ce; static zend_object_handlers dimension_handlers_no_ArrayAccess_object_handlers; @@ -302,6 +340,11 @@ void zend_test_object_handlers_init(void) memcpy(&numeric_castable_no_operation_object_handlers, &std_object_handlers, sizeof(zend_object_handlers)); numeric_castable_no_operation_object_handlers.cast_object = numeric_castable_no_operation_cast_object; + vm_interrupt_comparable_ce = register_class_VmInterruptComparable(); + vm_interrupt_comparable_ce->create_object = vm_interrupt_comparable_object_create; + memcpy(&vm_interrupt_comparable_object_handlers, &std_object_handlers, sizeof(zend_object_handlers)); + vm_interrupt_comparable_object_handlers.compare = vm_interrupt_comparable_compare; + dimension_handlers_no_ArrayAccess_ce = register_class_DimensionHandlersNoArrayAccess(); dimension_handlers_no_ArrayAccess_ce->create_object = dimension_handlers_no_ArrayAccess_object_create; memcpy(&dimension_handlers_no_ArrayAccess_object_handlers, &std_object_handlers, sizeof(zend_object_handlers)); diff --git a/ext/zend_test/object_handlers.stub.php b/ext/zend_test/object_handlers.stub.php index a474908b1095..8c8e7f9bfe9b 100644 --- a/ext/zend_test/object_handlers.stub.php +++ b/ext/zend_test/object_handlers.stub.php @@ -23,6 +23,11 @@ final class NumericCastableNoOperations { public function __construct(int|float $val) {} } +final class VmInterruptComparable { + private int $val; + public function __construct(int $val) {} +} + class DimensionHandlersNoArrayAccess { public bool $read = false; public bool $write = false; diff --git a/ext/zend_test/object_handlers_arginfo.h b/ext/zend_test/object_handlers_arginfo.h index 370ad13894aa..b46028a6e011 100644 --- a/ext/zend_test/object_handlers_arginfo.h +++ b/ext/zend_test/object_handlers_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 81be60f2c465ffe5c036739d072ab80d9c388907 */ + * Stub hash: 1a70ed60c5af38539b1222a979f97fddf7d1826e */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_DoOperationNoCast___construct, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, val, IS_LONG, 0) @@ -15,10 +15,13 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_NumericCastableNoOperations___construct, 0, ZEND_ARG_TYPE_MASK(0, val, MAY_BE_LONG|MAY_BE_DOUBLE, NULL) ZEND_END_ARG_INFO() +#define arginfo_class_VmInterruptComparable___construct arginfo_class_DoOperationNoCast___construct + static ZEND_METHOD(DoOperationNoCast, __construct); static ZEND_METHOD(LongCastableNoOperations, __construct); static ZEND_METHOD(FloatCastableNoOperations, __construct); static ZEND_METHOD(NumericCastableNoOperations, __construct); +static ZEND_METHOD(VmInterruptComparable, __construct); static const zend_function_entry class_DoOperationNoCast_methods[] = { ZEND_ME(DoOperationNoCast, __construct, arginfo_class_DoOperationNoCast___construct, ZEND_ACC_PUBLIC) @@ -40,6 +43,11 @@ static const zend_function_entry class_NumericCastableNoOperations_methods[] = { ZEND_FE_END }; +static const zend_function_entry class_VmInterruptComparable_methods[] = { + ZEND_ME(VmInterruptComparable, __construct, arginfo_class_VmInterruptComparable___construct, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + static zend_class_entry *register_class_DoOperationNoCast(void) { zend_class_entry ce, *class_entry; @@ -104,6 +112,22 @@ static zend_class_entry *register_class_NumericCastableNoOperations(void) return class_entry; } +static zend_class_entry *register_class_VmInterruptComparable(void) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "VmInterruptComparable", class_VmInterruptComparable_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); + + zval property_val_default_value; + ZVAL_UNDEF(&property_val_default_value); + zend_string *property_val_name = zend_string_init("val", sizeof("val") - 1, 1); + zend_declare_typed_property(class_entry, property_val_name, &property_val_default_value, ZEND_ACC_PRIVATE, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_val_name); + + return class_entry; +} + static zend_class_entry *register_class_DimensionHandlersNoArrayAccess(void) { zend_class_entry ce, *class_entry; diff --git a/ext/zend_test/tests/observer_vm_interrupt_tailcall_helper.phpt b/ext/zend_test/tests/observer_vm_interrupt_tailcall_helper.phpt new file mode 100644 index 000000000000..d0178bdbf614 --- /dev/null +++ b/ext/zend_test/tests/observer_vm_interrupt_tailcall_helper.phpt @@ -0,0 +1,26 @@ +--TEST-- +Observer: VM interrupt during tailcall helper dispatch +--DESCRIPTION-- +This exercises a VM interrupt raised while an opcode handler dispatches to an +extra-argument helper. On the tailcall VM, the helper may return an opline +tagged with ZEND_VM_ENTER_BIT; treating that tagged value as a zend_op * before +tailcalling the next handler can crash. +--EXTENSIONS-- +zend_test +--INI-- +opcache.jit=0 +zend_test.observer.set_vm_interrupt_on_begin=1 +--FILE-- + +--EXPECT-- +stdClass From 09ca095aa8e5de7ce23f82b116d7def76d75785b Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 18 May 2026 14:31:10 -0400 Subject: [PATCH 5/6] Fix GH-18422: int overflow in php_date_llabs php_date_llabs negated its argument with -i, which is UB when i is LLONG_MIN. Cast to uint64_t before negating and use stdint types throughout. Updated Y/x/X format call sites to PRIu64. Fixes GH-18422 Closes GH-21638 --- NEWS | 3 +++ ext/date/config0.m4 | 4 ++++ ext/date/php_date.c | 14 ++++---------- ext/date/tests/gh18422.phpt | 21 +++++++++++++++++++++ 4 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 ext/date/tests/gh18422.phpt diff --git a/NEWS b/NEWS index 3413ff97c2b5..ce2b34730978 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,9 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.4.22 +- Date: + . Fixed bug GH-18422 (int overflow in php_date_llabs). (iliaal) + - Intl: . Fix incorrect argument positions in out-of-bounds errors for IntlCalendar::set(), IntlCalendar::setDate(), IntlCalendar::setDateTime(), diff --git a/ext/date/config0.m4 b/ext/date/config0.m4 index 5af6be13faf7..9bd80e464f82 100644 --- a/ext/date/config0.m4 +++ b/ext/date/config0.m4 @@ -9,6 +9,10 @@ AX_CHECK_COMPILE_FLAG([-Wno-implicit-fallthrough], [-Werror]) PHP_DATE_CFLAGS="$PHP_DATE_CFLAGS -I@ext_builddir@/lib -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -DHAVE_TIMELIB_CONFIG_H=1" + +AX_CHECK_COMPILE_FLAG([-fwrapv], + [PHP_DATE_CFLAGS="$PHP_DATE_CFLAGS -fwrapv"]) + timelib_sources="lib/astro.c lib/dow.c lib/parse_date.c lib/parse_tz.c lib/parse_posix.c lib/timelib.c lib/tm2unixtime.c lib/unixtime2tm.c lib/parse_iso_intervals.c lib/interval.c" diff --git a/ext/date/php_date.c b/ext/date/php_date.c index acdd612d04c8..2fbfe6e14f7e 100644 --- a/ext/date/php_date.c +++ b/ext/date/php_date.c @@ -31,13 +31,7 @@ #include "win32/time.h" #endif -#ifdef PHP_WIN32 -static __inline __int64 php_date_llabs( __int64 i ) { return i >= 0? i: -i; } -#elif defined(__GNUC__) && __GNUC__ < 3 -static __inline __int64_t php_date_llabs( __int64_t i ) { return i >= 0 ? i : -i; } -#else -static inline long long php_date_llabs( long long i ) { return i >= 0 ? i : -i; } -#endif +static inline uint64_t php_date_llabs(int64_t i) { return i >= 0 ? (uint64_t)i : -(uint64_t)i; } #ifdef PHP_WIN32 #define DATE_I64_BUF_LEN 65 @@ -742,9 +736,9 @@ static zend_string *date_format(const char *format, size_t format_len, timelib_t /* year */ case 'L': length = slprintf(buffer, sizeof(buffer), "%d", timelib_is_leap((int) t->y)); break; case 'y': length = slprintf(buffer, sizeof(buffer), "%02d", (int) (t->y % 100)); break; - case 'Y': length = slprintf(buffer, sizeof(buffer), "%s%04lld", t->y < 0 ? "-" : "", php_date_llabs((timelib_sll) t->y)); break; - case 'x': length = slprintf(buffer, sizeof(buffer), "%s%04lld", t->y < 0 ? "-" : (t->y >= 10000 ? "+" : ""), php_date_llabs((timelib_sll) t->y)); break; - case 'X': length = slprintf(buffer, sizeof(buffer), "%s%04lld", t->y < 0 ? "-" : "+", php_date_llabs((timelib_sll) t->y)); break; + case 'Y': length = slprintf(buffer, sizeof(buffer), "%s%04" PRIu64, t->y < 0 ? "-" : "", php_date_llabs((timelib_sll) t->y)); break; + case 'x': length = slprintf(buffer, sizeof(buffer), "%s%04" PRIu64, t->y < 0 ? "-" : (t->y >= 10000 ? "+" : ""), php_date_llabs((timelib_sll) t->y)); break; + case 'X': length = slprintf(buffer, sizeof(buffer), "%s%04" PRIu64, t->y < 0 ? "-" : "+", php_date_llabs((timelib_sll) t->y)); break; /* time */ case 'a': length = slprintf(buffer, sizeof(buffer), "%s", t->h >= 12 ? "pm" : "am"); break; diff --git a/ext/date/tests/gh18422.phpt b/ext/date/tests/gh18422.phpt new file mode 100644 index 000000000000..643476615277 --- /dev/null +++ b/ext/date/tests/gh18422.phpt @@ -0,0 +1,21 @@ +--TEST-- +GH-18422 (int overflow in Date extension) +--FILE-- +format("Y"), "\n"; +echo $dto->format("x"), "\n"; +echo $dto->format("X"), "\n"; + +echo date_create("2024-06-15")->format("Y"), "\n"; +echo date_create("-0042-01-01")->format("Y"), "\n"; +?> +--EXPECTF-- +-%d +-%d +-%d +2024 +-0042 From d6b7bd0f778cd45c0bae91d6e334fdf9f7049199 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 16 May 2026 21:05:15 +0100 Subject: [PATCH 6/6] Fix GH-22062: SplDoublyLinkedList iterator UAF via destructor releasing next node. Pin the new traverse target via SPL_LLIST_CHECK_ADDREF before the shift/pop destructor runs. Otherwise a destructor that unlinks the next node (e.g. offsetUnset) frees it, leaving the iterator with a dangling pointer. close GH-22066 --- NEWS | 2 ++ ext/spl/spl_dllist.c | 3 ++- ext/spl/tests/gh22062.phpt | 29 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 ext/spl/tests/gh22062.phpt diff --git a/NEWS b/NEWS index 239becd194f6..1200d086067c 100644 --- a/NEWS +++ b/NEWS @@ -181,6 +181,8 @@ PHP NEWS with re-entrant getHash()). (Pratik Bhujel) . Fix bugs GH-8561, GH-8562, GH-8563, and GH-8564 (Fixing various SplFileObject iterator desync bugs). (iliaal) + . Fix bug GH-22062 (SplDoublyLinkedList iterator UAF + via destructor releasing next node). (David Carlier) - Sqlite3: . Fix NUL byte truncation in sqlite3 TEXT column handling. (ndossche) diff --git a/ext/spl/spl_dllist.c b/ext/spl/spl_dllist.c index eaadec03cf84..be4f13cfcfc5 100644 --- a/ext/spl/spl_dllist.c +++ b/ext/spl/spl_dllist.c @@ -798,6 +798,7 @@ static void spl_dllist_it_helper_move_forward(spl_ptr_llist_element **traverse_p if (flags & SPL_DLLIST_IT_LIFO) { *traverse_pointer_ptr = old->prev; + SPL_LLIST_CHECK_ADDREF(*traverse_pointer_ptr); (*traverse_position_ptr)--; if (flags & SPL_DLLIST_IT_DELETE) { @@ -808,6 +809,7 @@ static void spl_dllist_it_helper_move_forward(spl_ptr_llist_element **traverse_p } } else { *traverse_pointer_ptr = old->next; + SPL_LLIST_CHECK_ADDREF(*traverse_pointer_ptr); if (flags & SPL_DLLIST_IT_DELETE) { zval prev; @@ -820,7 +822,6 @@ static void spl_dllist_it_helper_move_forward(spl_ptr_llist_element **traverse_p } SPL_LLIST_DELREF(old); - SPL_LLIST_CHECK_ADDREF(*traverse_pointer_ptr); } } /* }}} */ diff --git a/ext/spl/tests/gh22062.phpt b/ext/spl/tests/gh22062.phpt new file mode 100644 index 000000000000..ea67a9983a3d --- /dev/null +++ b/ext/spl/tests/gh22062.phpt @@ -0,0 +1,29 @@ +--TEST-- +GH-22062 (SplDoublyLinkedList iterator UAF via destructor releasing next node) +--FILE-- +setIteratorMode( + SplDoublyLinkedList::IT_MODE_FIFO | + SplDoublyLinkedList::IT_MODE_DELETE +); + +$list->push(new class($list) { + public function __construct(private SplDoublyLinkedList $list) {} + public function __destruct() { + if ($this->list->count() > 0) { + $this->list->offsetUnset(0); + } + } +}); + +$list->push(new stdClass()); + +foreach ($list as $item) { + unset($item); +} + +var_dump($list->count()); +?> +--EXPECT-- +int(0)