diff --git a/NEWS b/NEWS
index cc52980d890b..653bcff9e05a 100644
--- a/NEWS
+++ b/NEWS
@@ -6,7 +6,16 @@ PHP NEWS
. Fixed bug GH-23242 (PHP development server does not support Expect
100-continue flow control). (Sjoerd Langkemper)
+- DOM:
+ . Fixed NamedNodeMap::getNamedItemNS() with an empty URI not matching
+ the null namespace in spec-following mode. (Ilia Alshanetsky)
+ . Fixed stale getElementsByClassName() and other node list caches after
+ className/classList writes and attribute removals. (Ilia Alshanetsky)
+
- Intl:
+ . Fixed a memory leak when dumping IntlCalendar instances. (Ilia Alshanetsky)
+ . Fixed Collator::sortWithSortKeys() allocating fixed 2MiB buffers
+ regardless of array size. (Ilia Alshanetsky)
. Fixed a memory leak when iterating IntlBreakIterator::getPartsIterator()
results. (iliaal)
. Fixed a leak in Locale::getKeywords() when a keyword value cannot be
@@ -33,6 +42,10 @@ PHP NEWS
which detached the classic BPF filter instead of the reuseport program.
(David Carlier)
+- SOAP:
+ . Fixed WSDL cache corruption when a soap:header defines headerfaults.
+ (Ilia Alshanetsky)
+
- Standard:
. Fixed a segfault when a stream filter callback unsets StreamBucket::$data
before re-attaching the bucket. (iliaal)
@@ -42,6 +55,10 @@ PHP NEWS
. Io\Poll\Context::wait() now rejects a $maxEvents value greater than
INT_MAX instead of truncating it. (marc-mabe)
+- SimpleXML:
+ . Fixed writing to a dimension of the object returned by attributes() not
+ creating the attribute. (Ilia Alshanetsky)
+
27 Aug 2026, PHP 8.6.0beta2
diff --git a/UPGRADING b/UPGRADING
index a164599c6f15..5a5cafc0234f 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -973,6 +973,7 @@ PHP 8.6 UPGRADE NOTES
. Reduced temporary allocations when iterating Phar directories.
- Standard:
+ . Improved performance of str_ends_with().
. Improved performance of array_fill_keys().
. Improved performance of array_intersect().
. Improved performance of array_map() with multiple arrays passed.
diff --git a/ext/dom/element.c b/ext/dom/element.c
index 354466623ca4..5fcffaf42055 100644
--- a/ext/dom/element.c
+++ b/ext/dom/element.c
@@ -154,6 +154,7 @@ static xmlAttrPtr dom_element_reflected_attribute_write(dom_object *obj, zval *n
/* Typed property, so it is a string already */
ZEND_ASSERT(Z_TYPE_P(newval) == IS_STRING);
+ php_libxml_invalidate_node_list_cache(obj->document);
return xmlSetNsProp(nodep, NULL, (const xmlChar *) name, (const xmlChar *) Z_STRVAL_P(newval));
}
@@ -542,7 +543,7 @@ static void dom_deep_ns_redef(xmlNodePtr node, xmlNsPtr ns_to_redefine)
efree(worklist);
}
-static bool dom_remove_attribute(xmlNodePtr thisp, xmlNodePtr attrp)
+static bool dom_remove_attribute(xmlNodePtr thisp, xmlNodePtr attrp, php_libxml_ref_obj *document)
{
ZEND_ASSERT(thisp != NULL);
ZEND_ASSERT(attrp != NULL);
@@ -597,6 +598,7 @@ static bool dom_remove_attribute(xmlNodePtr thisp, xmlNodePtr attrp)
return false;
default: ZEND_UNREACHABLE();
}
+ php_libxml_invalidate_node_list_cache(document);
return true;
}
@@ -622,7 +624,7 @@ PHP_METHOD(DOMElement, removeAttribute)
RETURN_FALSE;
}
- RETURN_BOOL(dom_remove_attribute(nodep, attrp));
+ RETURN_BOOL(dom_remove_attribute(nodep, attrp, intern->document));
}
PHP_METHOD(Dom_Element, removeAttribute)
@@ -640,7 +642,7 @@ PHP_METHOD(Dom_Element, removeAttribute)
attrp = dom_get_attribute_or_nsdecl(intern, nodep, BAD_CAST name, name_len);
if (attrp != NULL) {
- dom_remove_attribute(nodep, attrp);
+ dom_remove_attribute(nodep, attrp, intern->document);
}
}
/* }}} end dom_element_remove_attribute */
@@ -798,6 +800,7 @@ static void dom_element_remove_attribute_node(INTERNAL_FUNCTION_PARAMETERS, zend
RETURN_FALSE;
}
+ php_libxml_invalidate_node_list_cache(intern->document);
xmlUnlinkNode((xmlNodePtr) attrp);
DOM_RET_OBJ((xmlNodePtr) attrp, intern);
@@ -1198,6 +1201,7 @@ PHP_METHOD(DOMElement, removeAttributeNS)
if (nsptr != NULL) {
if (xmlStrEqual(BAD_CAST uri, nsptr->href)) {
dom_eliminate_ns(nodep, nsptr);
+ php_libxml_invalidate_node_list_cache(intern->document);
} else {
return;
}
@@ -1212,6 +1216,7 @@ PHP_METHOD(DOMElement, removeAttributeNS)
} else {
xmlUnlinkNode((xmlNodePtr) attrp);
}
+ php_libxml_invalidate_node_list_cache(intern->document);
}
}
/* }}} end dom_element_remove_attribute_ns */
@@ -1947,7 +1952,7 @@ PHP_METHOD(DOMElement, toggleAttribute)
/* Step 5 */
if (force_is_null || !force) {
- retval = !dom_remove_attribute(thisp, attribute);
+ retval = !dom_remove_attribute(thisp, attribute, intern->document);
goto out;
}
diff --git a/ext/dom/namednodemap.c b/ext/dom/namednodemap.c
index c6d8157881a3..d665bbbaebdc 100644
--- a/ext/dom/namednodemap.c
+++ b/ext/dom/namednodemap.c
@@ -106,6 +106,9 @@ PHP_METHOD(DOMNamedNodeMap, getNamedItemNS)
objmap = (dom_nnodemap_object *)intern->ptr;
if (objmap != NULL) {
+ if (urilen == 0 && objmap->baseobj != NULL && php_dom_follow_spec_intern(objmap->baseobj)) {
+ uri = NULL;
+ }
php_dom_obj_map_get_ns_named_item_into_zval(objmap, named, uri, return_value);
}
}
diff --git a/ext/dom/obj_map.c b/ext/dom/obj_map.c
index 4d6479e003f9..14521f333263 100644
--- a/ext/dom/obj_map.c
+++ b/ext/dom/obj_map.c
@@ -514,7 +514,11 @@ static xmlNodePtr dom_map_get_ns_named_item_prop(dom_nnodemap_object *map, const
xmlNodePtr nodep = dom_object_get_node(map->baseobj);
if (nodep) {
if (ns) {
- return (xmlNodePtr) xmlHasNsProp(nodep, BAD_CAST ZSTR_VAL(named), BAD_CAST ns);
+ xmlNodePtr itemnode = (xmlNodePtr) xmlHasNsProp(nodep, BAD_CAST ZSTR_VAL(named), BAD_CAST ns);
+ if (itemnode != NULL && itemnode->type == XML_ATTRIBUTE_DECL) {
+ return NULL;
+ }
+ return itemnode;
} else {
if (php_dom_follow_spec_intern(map->baseobj)) {
return (xmlNodePtr) php_dom_get_attribute_node(nodep, BAD_CAST ZSTR_VAL(named), ZSTR_LEN(named));
diff --git a/ext/dom/tests/modern/common/getElementsByClassName_cache_invalidation.phpt b/ext/dom/tests/modern/common/getElementsByClassName_cache_invalidation.phpt
new file mode 100644
index 000000000000..4efdad1b59b4
--- /dev/null
+++ b/ext/dom/tests/modern/common/getElementsByClassName_cache_invalidation.phpt
@@ -0,0 +1,44 @@
+--TEST--
+getElementsByClassName() cache must be invalidated by class attribute mutations
+--EXTENSIONS--
+dom
+--FILE--
+
$body");
+}
+
+$checks = [
+ 'className' => function ($doc, $span) { $span->className = 'zzz'; },
+ 'classList-remove' => function ($doc, $span) { $span->classList->remove('foo'); },
+ 'classList-value' => function ($doc, $span) { $span->classList->value = 'zzz'; },
+ 'setAttribute' => function ($doc, $span) { $span->setAttribute('class', 'zzz'); },
+ 'removeAttribute' => function ($doc, $span) { $span->removeAttribute('class'); },
+ 'removeAttributeNode' => function ($doc, $span) { $span->removeAttributeNode($span->attributes['class']); },
+];
+foreach ($checks as $label => $fn) {
+ $doc = mk('');
+ $coll = $doc->getElementsByClassName('foo');
+ if ($coll->length !== 1) {
+ echo "$label: unexpected initial length\n";
+ continue;
+ }
+ $fn($doc, $doc->querySelector('span'));
+ echo "$label: ", $coll->length === 0 ? "OK" : "STALE {$coll->length}", "\n";
+}
+
+$doc = mk('');
+$coll = $doc->getElementsByClassName('foo');
+var_dump($coll->length);
+$doc->querySelector('span')->className = 'foo';
+echo $coll->length === 1 ? "growth OK" : "growth STALE", "\n";
+?>
+--EXPECT--
+className: OK
+classList-remove: OK
+classList-value: OK
+setAttribute: OK
+removeAttribute: OK
+removeAttributeNode: OK
+int(0)
+growth OK
diff --git a/ext/dom/tests/modern/spec/NamedNodeMap_getNamedItemNS.phpt b/ext/dom/tests/modern/spec/NamedNodeMap_getNamedItemNS.phpt
new file mode 100644
index 000000000000..17d0659678e6
--- /dev/null
+++ b/ext/dom/tests/modern/spec/NamedNodeMap_getNamedItemNS.phpt
@@ -0,0 +1,25 @@
+--TEST--
+getNamedItemNS() with an empty URI must look up the null namespace
+--EXTENSIONS--
+dom
+--FILE--
+loadXML('');
+$a = $d->documentElement->attributes->getNamedItemNS('', 'bar');
+var_dump($a === null ? null : $a->nodeValue);
+$b = $d->documentElement->attributes->getNamedItemNS('urn:q', 'bar');
+var_dump($b === null ? null : $b->nodeValue);
+$d2 = Dom\XMLDocument::createFromString('');
+$a2 = $d2->documentElement->attributes->getNamedItemNS('', 'bar');
+var_dump($a2 === null ? null : $a2->nodeValue);
+var_dump($d2->documentElement->hasAttributeNS('', 'bar'));
+$c = $d2->documentElement->attributes->getNamedItemNS('urn:q', 'bar');
+var_dump($c === null ? null : $c->nodeValue);
+?>
+--EXPECT--
+NULL
+string(2) "ns"
+string(5) "no-ns"
+bool(true)
+string(2) "ns"
diff --git a/ext/dom/tests/modern/spec/NamedNodeMap_getNamedItemNS_dtd_default.phpt b/ext/dom/tests/modern/spec/NamedNodeMap_getNamedItemNS_dtd_default.phpt
new file mode 100644
index 000000000000..c661af974c77
--- /dev/null
+++ b/ext/dom/tests/modern/spec/NamedNodeMap_getNamedItemNS_dtd_default.phpt
@@ -0,0 +1,24 @@
+--TEST--
+getNamedItemNS() with empty URI must not throw on DTD default attributes
+--EXTENSIONS--
+dom
+--FILE--
+
+
+
+]>
+
+XML;
+
+$el = Dom\XMLDocument::createFromString($xml)->documentElement;
+$defaulted = $el->attributes->getNamedItemNS('', 'defaulted');
+var_dump($defaulted === null ? null : $defaulted->nodeValue);
+$real = $el->attributes->getNamedItemNS('', 'real');
+var_dump($real === null ? null : $real->nodeValue);
+?>
+--EXPECT--
+NULL
+string(7) "present"
diff --git a/ext/dom/token_list.c b/ext/dom/token_list.c
index 0e7797616554..30f308a14d67 100644
--- a/ext/dom/token_list.c
+++ b/ext/dom/token_list.c
@@ -182,6 +182,7 @@ static void dom_token_list_update(dom_token_list_object *intern)
HashTable *token_set = TOKEN_LIST_GET_SET(intern);
php_libxml_invalidate_cache_tag(&intern->cache_tag);
+ php_libxml_invalidate_node_list_cache(intern->dom.document);
/* 1. If the associated element does not have an associated attribute and token set is empty, then return. */
if (attr == NULL && zend_hash_num_elements(token_set) == 0) {
@@ -430,6 +431,7 @@ zend_result dom_token_list_value_write(dom_object *obj, zval *newval)
zend_value_error("Value must not contain any null bytes");
return FAILURE;
}
+ php_libxml_invalidate_node_list_cache(intern->dom.document);
xmlSetNsProp(dom_token_list_get_element(intern), NULL, BAD_CAST "class", BAD_CAST Z_STRVAL_P(newval));
/* Note: we don't update the set here, the set is always lazily updated for performance reasons. */
return SUCCESS;
diff --git a/ext/intl/calendar/calendar_class.cpp b/ext/intl/calendar/calendar_class.cpp
index c74f0ab9412f..46e8ba49b89c 100644
--- a/ext/intl/calendar/calendar_class.cpp
+++ b/ext/intl/calendar/calendar_class.cpp
@@ -171,6 +171,8 @@ static HashTable *Calendar_get_debug_info(zend_object *object, int *is_temp)
FREE_HASHTABLE(debug_info_tz);
zend_hash_str_update(debug_info, "timeZone", sizeof("timeZone") - 1, &ztz_debug);
+
+ zval_ptr_dtor(&ztz);
}
{
diff --git a/ext/intl/collator/collator_sort.cpp b/ext/intl/collator/collator_sort.cpp
index cb1f2aefc358..f2674b9c8ff8 100644
--- a/ext/intl/collator/collator_sort.cpp
+++ b/ext/intl/collator/collator_sort.cpp
@@ -44,9 +44,8 @@ ZEND_EXTERN_MODULE_GLOBALS( intl )
static const size_t DEF_SORT_KEYS_BUF_SIZE = 1048576;
static const size_t DEF_SORT_KEYS_BUF_INCREMENT = 1048576;
-
-static const size_t DEF_SORT_KEYS_INDX_BUF_SIZE = 1048576;
-static const size_t DEF_SORT_KEYS_INDX_BUF_INCREMENT = 1048576;
+static const size_t MIN_SORT_KEYS_BUF_SIZE = 4096;
+static const size_t SORT_KEY_LENGTH_ESTIMATE = 32;
static const size_t DEF_UTF16_BUF_SIZE = 1024;
@@ -427,17 +426,17 @@ U_CFUNC PHP_FUNCTION( collator_sort_with_sort_keys )
zval* hashData = nullptr; /* currently processed item of input hash */
char* sortKeyBuf = nullptr; /* buffer to store sort keys */
- uint32_t sortKeyBufSize = DEF_SORT_KEYS_BUF_SIZE; /* buffer size */
+ uint32_t sortKeyBufSize = 0; /* buffer size */
ptrdiff_t sortKeyBufOffset = 0; /* pos in buffer to store sort key */
uint32_t sortKeyLen = 0; /* the length of currently processing key */
uint32_t bufLeft = 0;
uint32_t bufIncrement = 0;
collator_sort_key_index_t* sortKeyIndxBuf = nullptr; /* buffer to store 'indexes' which will be passed to 'qsort' */
- uint32_t sortKeyIndxBufSize = DEF_SORT_KEYS_INDX_BUF_SIZE;
uint32_t sortKeyIndxSize = sizeof( collator_sort_key_index_t );
uint32_t sortKeyCount = 0;
+ uint32_t numElements = 0;
uint32_t j = 0;
UChar* utf16_buf = nullptr; /* tmp buffer to hold current processing string in utf-16 */
@@ -472,9 +471,20 @@ U_CFUNC PHP_FUNCTION( collator_sort_with_sort_keys )
if( !hash || zend_hash_num_elements( hash ) == 0 )
RETURN_TRUE;
+ numElements = zend_hash_num_elements( hash );
+
+ if( numElements > DEF_SORT_KEYS_BUF_SIZE / SORT_KEY_LENGTH_ESTIMATE ) {
+ sortKeyBufSize = DEF_SORT_KEYS_BUF_SIZE;
+ } else {
+ sortKeyBufSize = numElements * SORT_KEY_LENGTH_ESTIMATE;
+ }
+ if( sortKeyBufSize < MIN_SORT_KEYS_BUF_SIZE ) {
+ sortKeyBufSize = MIN_SORT_KEYS_BUF_SIZE;
+ }
+
/* Create buffers */
- sortKeyBuf = reinterpret_cast(ecalloc( sortKeyBufSize, sizeof( char ) ));
- sortKeyIndxBuf = reinterpret_cast(ecalloc( sortKeyIndxBufSize, sizeof( uint8_t ) ));
+ sortKeyBuf = reinterpret_cast(ecalloc( sortKeyBufSize, sizeof( char ) ));
+ sortKeyIndxBuf = reinterpret_cast(ecalloc( numElements, sortKeyIndxSize ));
utf16_buf = eumalloc( utf16_buf_size );
/* Iterate through input hash and create a sort key for each value. */
@@ -524,7 +534,15 @@ U_CFUNC PHP_FUNCTION( collator_sort_with_sort_keys )
/* check for sortKeyBuf overflow, increasing its size of the buffer if needed */
if( sortKeyLen > bufLeft )
{
- bufIncrement = ( sortKeyLen > DEF_SORT_KEYS_BUF_INCREMENT ) ? sortKeyLen : DEF_SORT_KEYS_BUF_INCREMENT;
+ bufIncrement = sortKeyBufSize;
+
+ if( bufIncrement > DEF_SORT_KEYS_BUF_INCREMENT ) {
+ bufIncrement = DEF_SORT_KEYS_BUF_INCREMENT;
+ }
+
+ if( bufIncrement < sortKeyLen ) {
+ bufIncrement = sortKeyLen;
+ }
sortKeyBufSize += bufIncrement;
bufLeft += bufIncrement;
@@ -534,16 +552,6 @@ U_CFUNC PHP_FUNCTION( collator_sort_with_sort_keys )
sortKeyLen = ucol_getSortKey( co->ucoll, utf16_buf, utf16_len, (uint8_t*)sortKeyBuf + sortKeyBufOffset, bufLeft );
}
- /* check sortKeyIndxBuf overflow, increasing its size of the buffer if needed */
- if( ( sortKeyCount + 1 ) * sortKeyIndxSize > sortKeyIndxBufSize )
- {
- bufIncrement = ( sortKeyIndxSize > DEF_SORT_KEYS_INDX_BUF_INCREMENT ) ? sortKeyIndxSize : DEF_SORT_KEYS_INDX_BUF_INCREMENT;
-
- sortKeyIndxBufSize += bufIncrement;
-
- sortKeyIndxBuf = reinterpret_cast(erealloc( sortKeyIndxBuf, sortKeyIndxBufSize ));
- }
-
sortKeyIndxBuf[sortKeyCount].key = (char*)sortKeyBufOffset; /* remember just offset, cause address */
/* of 'sortKeyBuf' may be changed due to realloc. */
sortKeyIndxBuf[sortKeyCount].zstr = hashData;
diff --git a/ext/intl/tests/calendar_get_debug_info_tz_leak.phpt b/ext/intl/tests/calendar_get_debug_info_tz_leak.phpt
new file mode 100644
index 000000000000..32b9da4368a9
--- /dev/null
+++ b/ext/intl/tests/calendar_get_debug_info_tz_leak.phpt
@@ -0,0 +1,26 @@
+--TEST--
+IntlCalendar get_debug_info() must not leak the time zone wrapper object
+--EXTENSIONS--
+intl
+--FILE--
+
+--EXPECT--
+int(0)
diff --git a/ext/intl/tests/collator_sort_with_sort_keys_buffer_size.phpt b/ext/intl/tests/collator_sort_with_sort_keys_buffer_size.phpt
new file mode 100644
index 000000000000..ef1d68851e37
--- /dev/null
+++ b/ext/intl/tests/collator_sort_with_sort_keys_buffer_size.phpt
@@ -0,0 +1,57 @@
+--TEST--
+Collator::sortWithSortKeys() buffer allocation scales with array size
+--EXTENSIONS--
+intl
+--FILE--
+sort($a);
+
+$before = memory_get_peak_usage();
+$b = ['bb', 'aa', 'cc', 'ab', 'ca', 'bc', 'ac', 'ba'];
+$c->sortWithSortKeys($b);
+$peakDelta = memory_get_peak_usage() - $before;
+
+var_dump($a);
+var_dump($b);
+var_dump($peakDelta < 100000);
+
+$long = str_repeat('a', 10000);
+$d = [$long . 'b', $long . 'a'];
+$c->sortWithSortKeys($d);
+echo $d[0] === $long . 'a' ? "long-a\n" : "fail-a\n";
+echo $d[1] === $long . 'b' ? "long-b\n" : "fail-b\n";
+?>
+--EXPECT--
+array(4) {
+ [0]=>
+ string(2) "aa"
+ [1]=>
+ string(2) "bb"
+ [2]=>
+ string(2) "cc"
+ [3]=>
+ string(2) "dd"
+}
+array(8) {
+ [0]=>
+ string(2) "aa"
+ [1]=>
+ string(2) "ab"
+ [2]=>
+ string(2) "ac"
+ [3]=>
+ string(2) "ba"
+ [4]=>
+ string(2) "bb"
+ [5]=>
+ string(2) "bc"
+ [6]=>
+ string(2) "ca"
+ [7]=>
+ string(2) "cc"
+}
+bool(true)
+long-a
+long-b
diff --git a/ext/mysqli/tests/fake_server.inc b/ext/mysqli/tests/fake_server.inc
index dad8bc52ddd1..3af5c7459159 100644
--- a/ext/mysqli/tests/fake_server.inc
+++ b/ext/mysqli/tests/fake_server.inc
@@ -721,6 +721,19 @@ function my_mysqli_test_auth_response_message_over_read(my_mysqli_fake_server_co
$conn->read();
}
+function my_mysqli_test_ok_packet_message_over_read(my_mysqli_fake_server_conn $conn): void
+{
+ $p = new my_mysqli_fake_packet();
+ $p->full = "08000001" . "00" . "00" . "00" . "0200" . "0000" . "fa";
+
+ $conn->send_server_greetings();
+ $conn->read_packets(1);
+ $conn->send_server_ok();
+ $conn->read_packets(1);
+ $conn->send($p->to_bytes(), "Malicious OK Packet [message length past the packet size]");
+ $conn->read();
+}
+
function my_mysqli_test_stmt_response_row_over_read_string(my_mysqli_fake_server_conn $conn): void
{
$rh = $conn->packet_generator->server_stmt_execute_items_response();
@@ -816,6 +829,50 @@ function my_mysqli_test_stmt_response_row_read_two_fields(my_mysqli_fake_server_
}
}
+function my_mysqli_test_rset_field_metadata_len_over_read(my_mysqli_fake_server_conn $conn): void
+{
+ $rh = $conn->packet_generator->server_tabular_query_response();
+
+ $qr2 = new my_mysqli_fake_packet();
+ $qr2->packet_length = "0c0000";
+ $qr2->packet_number = "02";
+ $qr2->catalog_length_plus_name = "0161";
+ $qr2->db_length_plus_name = "0162";
+ $qr2->table_length_plus_name = "0163";
+ $qr2->original_t = "0164";
+ $qr2->name_length_plus_name = "0165";
+ $qr2->original_n = "fcff";
+
+ $conn->send_server_greetings();
+ $conn->read_packets(1);
+ $conn->send_server_ok();
+ $conn->read_packets(1);
+ $conn->send($conn->packets_to_bytes([$rh[0], $qr2]), "Malicious Tabular Response [metadata string length past the packet size]");
+ $conn->read();
+}
+
+function my_mysqli_test_rset_field_metadata_len_past_packet(my_mysqli_fake_server_conn $conn): void
+{
+ $rh = $conn->packet_generator->server_tabular_query_response();
+
+ $qr2 = new my_mysqli_fake_packet();
+ $qr2->packet_length = "0c0000";
+ $qr2->packet_number = "02";
+ $qr2->catalog_length_plus_name = "0161";
+ $qr2->db_length_plus_name = "0162";
+ $qr2->table_length_plus_name = "0163";
+ $qr2->original_t = "0164";
+ $qr2->name_length_plus_name = "0165";
+ $qr2->original_n = "0561";
+
+ $conn->send_server_greetings();
+ $conn->read_packets(1);
+ $conn->send_server_ok();
+ $conn->read_packets(1);
+ $conn->send($conn->packets_to_bytes([$rh[0], $qr2]), "Malicious Tabular Response [metadata string length past the packet size]");
+ $conn->read();
+}
+
function my_mysqli_test_query_response_row_length_overflow(my_mysqli_fake_server_conn $conn): void
{
$rh = $conn->packet_generator->server_query_execute_data_response('strval');
diff --git a/ext/mysqli/tests/mysqlnd_ok_packet_message_over_read.phpt b/ext/mysqli/tests/mysqlnd_ok_packet_message_over_read.phpt
new file mode 100644
index 000000000000..8364251e8df4
--- /dev/null
+++ b/ext/mysqli/tests/mysqlnd_ok_packet_message_over_read.phpt
@@ -0,0 +1,40 @@
+--TEST--
+mysqlnd OK packet message length buffer over-read
+--EXTENSIONS--
+mysqli
+--FILE--
+wait();
+
+try {
+ $conn = new mysqli( $servername, $username, $password, "", $process->getPort());
+ var_dump($conn->select_db("test"));
+} catch (Exception $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+$process->terminate();
+
+print "done!";
+?>
+--EXPECTF--
+[*] Server started on 127.0.0.1:%d
+[*] Connection established
+[*] Sending - Server Greeting: %s
+[*] Received: %s
+[*] Sending - Server OK: %s
+[*] Received: %s
+[*] Sending - Malicious OK Packet [message length past the packet size]: %s
+
+Warning: mysqli::select_db(): OK packet message length is past the packet size in %s on line %d
+
+Warning: mysqli::select_db(): Error while reading INIT_DB's response packet. PID=%d in %s on line %d
+mysqli_sql_exception: Malformed packet
+done!
diff --git a/ext/mysqli/tests/mysqlnd_rset_field_len_over_read.phpt b/ext/mysqli/tests/mysqlnd_rset_field_len_over_read.phpt
new file mode 100644
index 000000000000..274468ded63d
--- /dev/null
+++ b/ext/mysqli/tests/mysqlnd_rset_field_len_over_read.phpt
@@ -0,0 +1,42 @@
+--TEST--
+mysqlnd result set field metadata string length buffer over-read (len clamped to packet size)
+--EXTENSIONS--
+mysqli
+--FILE--
+wait();
+
+try {
+ $conn = new mysqli( $servername, $username, $password, "", $process->getPort());
+ var_dump($conn->query("SELECT * from users"));
+} catch (Exception $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+$conn->close();
+
+$process->terminate();
+
+print "done!";
+?>
+--EXPECTF--
+[*] Server started on 127.0.0.1:%d
+[*] Connection established
+[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264
+[*] Received: %s
+[*] Sending - Server OK: 0700000200000002000000
+[*] Received: %s
+[*] Sending - Malicious Tabular Response [metadata string length past the packet size]: 01000001010c00000201610162016301640165fcff
+
+Warning: mysqli::query(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d
+
+Warning: mysqli::query(): Result set field packet %d bytes shorter than expected in %s on line %d
+bool(false)
+done!
diff --git a/ext/mysqli/tests/mysqlnd_rset_field_len_past_packet.phpt b/ext/mysqli/tests/mysqlnd_rset_field_len_past_packet.phpt
new file mode 100644
index 000000000000..020d28d95b25
--- /dev/null
+++ b/ext/mysqli/tests/mysqlnd_rset_field_len_past_packet.phpt
@@ -0,0 +1,40 @@
+--TEST--
+mysqlnd result set field metadata string length exceeds remaining packet bytes
+--EXTENSIONS--
+mysqli
+--FILE--
+wait();
+
+try {
+ $conn = new mysqli( $servername, $username, $password, "", $process->getPort());
+ var_dump($conn->query("SELECT * from users"));
+} catch (Exception $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+$conn->close();
+
+$process->terminate();
+
+print "done!";
+?>
+--EXPECTF--
+[*] Server started on 127.0.0.1:%d
+[*] Connection established
+[*] Sending - Server Greeting: %s
+[*] Received: %s
+[*] Sending - Server OK: %s
+[*] Received: %s
+[*] Sending - Malicious Tabular Response [metadata string length past the packet size]: %s
+
+Warning: mysqli::query(): Result set field metadata string length is past the packet size in %s on line %d
+bool(false)
+done!
diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.c b/ext/mysqlnd/mysqlnd_wireprotocol.c
index b957240a4088..094daa939aa4 100644
--- a/ext/mysqlnd/mysqlnd_wireprotocol.c
+++ b/ext/mysqlnd/mysqlnd_wireprotocol.c
@@ -876,7 +876,12 @@ php_mysqlnd_ok_read(MYSQLND_CONN_DATA * conn, void * _packet)
/* There is a message */
if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p))) {
- packet->message_len = MIN(net_len, buf_len - (p - begin));
+ if ((p - buf) > packet->header.size || packet->header.size - (p - buf) < net_len) {
+ DBG_ERR_FMT("OK packet message length is past the packet size");
+ php_error_docref(NULL, E_WARNING, "OK packet message length is past the packet size");
+ DBG_RETURN(FAIL);
+ }
+ packet->message_len = net_len;
packet->message = mnd_pestrndup((char *)p, packet->message_len, FALSE);
} else {
packet->message = NULL;
@@ -1169,10 +1174,17 @@ void php_mysqlnd_rset_header_free_mem(void * _packet)
/* }}} */
#define READ_RSET_FIELD(field_name) do { \
+ BAIL_IF_NO_MORE_DATA; \
len = php_mysqlnd_net_field_length(&p); \
if (UNEXPECTED(len == MYSQLND_NULL_LENGTH)) { \
goto faulty_or_fake; \
} else if (len != 0) { \
+ BAIL_IF_NO_MORE_DATA; \
+ if (UNEXPECTED((p - begin) > packet->header.size || packet->header.size - (p - begin) < len)) { \
+ DBG_ERR_FMT("Result set field metadata string length is past the packet size"); \
+ php_error_docref(NULL, E_WARNING, "Result set field metadata string length is past the packet size"); \
+ DBG_RETURN(FAIL); \
+ } \
meta->field_name = (const char *)p; \
meta->field_name ## _length = len; \
p += len; \
@@ -1241,7 +1253,7 @@ php_mysqlnd_rset_field_read(MYSQLND_CONN_DATA * conn, void * _packet)
READ_RSET_FIELD(name);
READ_RSET_FIELD(org_name);
- /* 1 byte length */
+ BAIL_IF_NO_MORE_DATA;
if (UNEXPECTED(12 != *p)) {
DBG_ERR_FMT("Protocol error. Server sent false length. Expected 12 got %d", (int) *p);
php_error_docref(NULL, E_WARNING, "Protocol error. Server sent false length. Expected 12");
diff --git a/ext/simplexml/simplexml.c b/ext/simplexml/simplexml.c
index 94c538a40488..74d310e9d938 100644
--- a/ext/simplexml/simplexml.c
+++ b/ext/simplexml/simplexml.c
@@ -436,8 +436,7 @@ static zval *sxe_prop_dim_write(zend_object *object, zval *member, zval *value,
if (sxe->iter.type == SXE_ITER_ATTRLIST) {
access_mode = SXE_ACCESS_ATTRIBS;
- node = php_sxe_get_first_node_non_destructive(sxe, node);
- attr = (xmlAttrPtr)node;
+ attr = (xmlAttrPtr)php_sxe_get_first_node_non_destructive(sxe, node);
test = sxe->iter.name != NULL;
} else if (sxe->iter.type != SXE_ITER_CHILD) {
mynode = node;
diff --git a/ext/simplexml/tests/attributes_dimension_write.phpt b/ext/simplexml/tests/attributes_dimension_write.phpt
new file mode 100644
index 000000000000..8721dc7dc7c2
--- /dev/null
+++ b/ext/simplexml/tests/attributes_dimension_write.phpt
@@ -0,0 +1,30 @@
+--TEST--
+Creating new attributes via dimension and property writes on attributes()
+--FILE--
+');
+$x->attributes()['new'] = 'v';
+echo $x->asXML();
+
+$a = simplexml_load_string('');
+$a->attributes()['created'] = 'yes';
+echo $a->asXML();
+
+$b = simplexml_load_string('');
+$attrs = $b->attributes();
+$attrs->other = 2;
+echo $b->asXML();
+
+$c = simplexml_load_string('');
+$c->attributes()['a'] = '2';
+echo $c->asXML();
+?>
+--EXPECT--
+
+
+
+
+
+
+
+
diff --git a/ext/soap/php_sdl.c b/ext/soap/php_sdl.c
index c6e53c408aa8..3062a2d4dbf6 100644
--- a/ext/soap/php_sdl.c
+++ b/ext/soap/php_sdl.c
@@ -1144,7 +1144,7 @@ static sdlPtr load_wsdl(zval *this_ptr, char *struri)
return ctx.sdl;
}
-#define WSDL_CACHE_VERSION 0x10
+#define WSDL_CACHE_VERSION 0x11
#define WSDL_CACHE_GET(ret,type,buf) memcpy(&ret,*buf,sizeof(type)); *buf += sizeof(type);
#define WSDL_CACHE_GET_INT(ret,buf) ret = ((unsigned char)(*buf)[0])|((unsigned char)(*buf)[1]<<8)|((unsigned char)(*buf)[2]<<16)|((unsigned)(*buf)[3]<<24); *buf += 4;
@@ -2054,7 +2054,7 @@ static void sdl_serialize_soap_body(const sdlSoapBindingFunctionBodyPtr body, co
sdlSoapBindingFunctionHeaderPtr tmp2;
const zend_string *key_inner;
- ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(body->headers, key_inner, tmp2) {
+ ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(tmp->headerfaults, key_inner, tmp2) {
sdl_serialize_key(key_inner, out);
WSDL_CACHE_PUT_1(tmp2->use, out);
if (tmp2->use == SOAP_ENCODED) {
diff --git a/ext/soap/tests/headerfault_cache.phpt b/ext/soap/tests/headerfault_cache.phpt
new file mode 100644
index 000000000000..6da747902d80
--- /dev/null
+++ b/ext/soap/tests/headerfault_cache.phpt
@@ -0,0 +1,45 @@
+--TEST--
+WSDL cache corruption when soap:header has headerfaults
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=1
+--FILE--
+ WSDL_CACHE_DISK];
+
+$c1 = new SoapClient(__DIR__ . '/headerfault_cache.wsdl', $options);
+var_dump($c1->__getFunctions());
+
+$c2 = new SoapClient(__DIR__ . '/headerfault_cache.wsdl', $options);
+var_dump($c2->__getFunctions());
+
+echo "ok\n";
+?>
+--CLEAN--
+
+--EXPECT--
+array(1) {
+ [0]=>
+ string(32) "string testHeader(string $param)"
+}
+array(1) {
+ [0]=>
+ string(32) "string testHeader(string $param)"
+}
+ok
diff --git a/ext/soap/tests/headerfault_cache.wsdl b/ext/soap/tests/headerfault_cache.wsdl
new file mode 100644
index 000000000000..8a844c0b899d
--- /dev/null
+++ b/ext/soap/tests/headerfault_cache.wsdl
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php
index 3e23934cbc78..8ae94d8c6d99 100644
--- a/ext/standard/basic_functions.stub.php
+++ b/ext/standard/basic_functions.stub.php
@@ -2451,7 +2451,10 @@ function str_contains(string $haystack, string $needle): bool {}
*/
function str_starts_with(string $haystack, string $needle): bool {}
-/** @compile-time-eval */
+/**
+ * @compile-time-eval
+ * @frameless-function {"arity": 2}
+ */
function str_ends_with(string $haystack, string $needle): bool {}
/**
diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h
index 442085e9d6cc..4bcf008f5fdd 100644
--- a/ext/standard/basic_functions_arginfo.h
+++ b/ext/standard/basic_functions_arginfo.h
@@ -1,5 +1,5 @@
/* This is a generated file, edit basic_functions.stub.php instead.
- * Stub hash: c645e310c00d9f4cb3856c94ee60d06071e28de0
+ * Stub hash: 31018a787ba261316941b0d88f090b9cf271aa0e
* Has decl header: yes */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0)
@@ -2294,6 +2294,12 @@ static const zend_frameless_function_info frameless_function_infos_str_starts_wi
{ 0 },
};
+ZEND_FRAMELESS_FUNCTION(str_ends_with, 2);
+static const zend_frameless_function_info frameless_function_infos_str_ends_with[] = {
+ { ZEND_FRAMELESS_FUNCTION_NAME(str_ends_with, 2), 2 },
+ { 0 },
+};
+
ZEND_FRAMELESS_FUNCTION(substr, 2);
ZEND_FRAMELESS_FUNCTION(substr, 3);
static const zend_frameless_function_info frameless_function_infos_substr[] = {
@@ -3197,7 +3203,7 @@ static const zend_function_entry ext_functions[] = {
ZEND_RAW_FENTRY("strrchr", zif_strrchr, arginfo_strrchr, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL)
ZEND_RAW_FENTRY("str_contains", zif_str_contains, arginfo_str_contains, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_str_contains, NULL)
ZEND_RAW_FENTRY("str_starts_with", zif_str_starts_with, arginfo_str_starts_with, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_str_starts_with, NULL)
- ZEND_RAW_FENTRY("str_ends_with", zif_str_ends_with, arginfo_str_ends_with, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL)
+ ZEND_RAW_FENTRY("str_ends_with", zif_str_ends_with, arginfo_str_ends_with, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_str_ends_with, NULL)
ZEND_RAW_FENTRY("chunk_split", zif_chunk_split, arginfo_chunk_split, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL)
ZEND_RAW_FENTRY("substr", zif_substr, arginfo_substr, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_substr, NULL)
ZEND_RAW_FENTRY("substr_replace", zif_substr_replace, arginfo_substr_replace, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL)
diff --git a/ext/standard/basic_functions_decl.h b/ext/standard/basic_functions_decl.h
index f2f234f60cc2..db81e0bfc077 100644
--- a/ext/standard/basic_functions_decl.h
+++ b/ext/standard/basic_functions_decl.h
@@ -1,8 +1,8 @@
/* This is a generated file, edit basic_functions.stub.php instead.
- * Stub hash: c645e310c00d9f4cb3856c94ee60d06071e28de0 */
+ * Stub hash: 31018a787ba261316941b0d88f090b9cf271aa0e */
-#ifndef ZEND_BASIC_FUNCTIONS_DECL_c645e310c00d9f4cb3856c94ee60d06071e28de0_H
-#define ZEND_BASIC_FUNCTIONS_DECL_c645e310c00d9f4cb3856c94ee60d06071e28de0_H
+#ifndef ZEND_BASIC_FUNCTIONS_DECL_31018a787ba261316941b0d88f090b9cf271aa0e_H
+#define ZEND_BASIC_FUNCTIONS_DECL_31018a787ba261316941b0d88f090b9cf271aa0e_H
typedef enum zend_enum_SortDirection {
ZEND_ENUM_SortDirection_Ascending = 1,
@@ -20,4 +20,4 @@ typedef enum zend_enum_RoundingMode {
ZEND_ENUM_RoundingMode_PositiveInfinity = 8,
} zend_enum_RoundingMode;
-#endif /* ZEND_BASIC_FUNCTIONS_DECL_c645e310c00d9f4cb3856c94ee60d06071e28de0_H */
+#endif /* ZEND_BASIC_FUNCTIONS_DECL_31018a787ba261316941b0d88f090b9cf271aa0e_H */
diff --git a/ext/standard/string.c b/ext/standard/string.c
index e5307a4f2d4b..af3f6a461dcf 100644
--- a/ext/standard/string.c
+++ b/ext/standard/string.c
@@ -1895,6 +1895,21 @@ PHP_FUNCTION(str_ends_with)
}
/* }}} */
+ZEND_FRAMELESS_FUNCTION(str_ends_with, 2)
+{
+ zval haystack_tmp, needle_tmp;
+ zend_string *haystack, *needle;
+
+ Z_FLF_PARAM_STR(1, haystack, haystack_tmp);
+ Z_FLF_PARAM_STR(2, needle, needle_tmp);
+
+ RETVAL_BOOL(zend_string_ends_with(haystack, needle));
+
+flf_clean:
+ Z_FLF_PARAM_FREE_STR(1, haystack_tmp);
+ Z_FLF_PARAM_FREE_STR(2, needle_tmp);
+}
+
static zend_always_inline void _zend_strpos(zval *return_value, zend_string *haystack, zend_string *needle, zend_long offset)
{
const char *found = NULL;
diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c
index a7a3e340ecf1..15994b60cc70 100644
--- a/ext/zip/php_zip.c
+++ b/ext/zip/php_zip.c
@@ -2197,7 +2197,7 @@ PHP_METHOD(ZipArchive, getNameIndex)
ZIP_FROM_OBJECT(intern, self);
- name = zip_get_name(intern, (int) index, flags);
+ name = zip_get_name(intern, (zip_uint64_t) index, flags);
if (name) {
RETVAL_STRING((char *)name);
diff --git a/ext/zip/tests/oo_getnameindex_large_index.phpt b/ext/zip/tests/oo_getnameindex_large_index.phpt
new file mode 100644
index 000000000000..471dffc38d91
--- /dev/null
+++ b/ext/zip/tests/oo_getnameindex_large_index.phpt
@@ -0,0 +1,44 @@
+--TEST--
+ZipArchive::getNameIndex() with an index that does not fit in an int
+--EXTENSIONS--
+zip
+--SKIPIF--
+
+--FILE--
+open($file, ZipArchive::CREATE)) {
+ exit('failed');
+}
+
+$zip->addFromString('entry1.txt', 'entry #1');
+$zip->close();
+
+if (!$zip->open($file)) {
+ exit('failed');
+}
+
+var_dump($zip->getNameIndex(0));
+var_dump($zip->getNameIndex(1 << 32));
+var_dump($zip->getNameIndex((1 << 32) + 1));
+var_dump($zip->getNameIndex(PHP_INT_MAX));
+var_dump($zip->getNameIndex(-1));
+
+$zip->close();
+?>
+--EXPECT--
+string(10) "entry1.txt"
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+--CLEAN--
+
diff --git a/ext/zip/tests/stream_fstat_unreadable_archive.phpt b/ext/zip/tests/stream_fstat_unreadable_archive.phpt
new file mode 100644
index 000000000000..a81fc8fc3cfc
--- /dev/null
+++ b/ext/zip/tests/stream_fstat_unreadable_archive.phpt
@@ -0,0 +1,38 @@
+--TEST--
+fstat() on a zip:// stream whose archive can no longer be opened
+--EXTENSIONS--
+zip
+--SKIPIF--
+
+--FILE--
+open($file, ZipArchive::CREATE)) {
+ exit('failed');
+}
+
+$zip->addFromString('entry.txt', 'entry');
+$zip->close();
+
+$fp = fopen('zip://' . $file . '#entry.txt', 'rb');
+var_dump($fp !== false);
+
+file_put_contents($file, 'this is not a zip archive');
+
+var_dump(fstat($fp));
+
+fclose($fp);
+?>
+--EXPECT--
+bool(true)
+bool(false)
+--CLEAN--
+
diff --git a/ext/zip/zip_stream.c b/ext/zip/zip_stream.c
index a6665630e350..429342b36e3d 100644
--- a/ext/zip/zip_stream.c
+++ b/ext/zip/zip_stream.c
@@ -192,6 +192,9 @@ static int php_zip_ops_stat(php_stream *stream, php_stream_statbuf *ssb) /* {{{
ssb->sb.st_blocks = -1;
#endif
ssb->sb.st_ino = -1;
+ } else {
+ zend_string_release_ex(file_basename, 0);
+ return -1;
}
zend_string_release_ex(file_basename, false);
return 0;