From d09e5e2bc2d194ff454bf7d737fe7ed7e369f62e Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 13 Aug 2026 09:08:52 -0400 Subject: [PATCH 1/6] Fix DOMNameSpaceNode clone UAF after xinclude Clone built the fake namespace decl from original_node->parent, which xinclude has already freed; parent_intern is the durable handle. Closes GH-23248 --- NEWS | 4 ++ ext/dom/php_dom.c | 14 +++--- .../dom_namespacenode_clone_xinclude.phpt | 44 +++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 ext/dom/tests/dom_namespacenode_clone_xinclude.phpt diff --git a/NEWS b/NEWS index ecb4d105e917..338409dd70c5 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,10 @@ PHP NEWS . Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or next() call on the inner generator). (iliaal) +- DOM: + . Fixed a use-after-free when cloning a DOMNameSpaceNode after + DOMDocument::xinclude(). (iliaal) + - Opcache: . Fixed opcache.protect_memory race under ZTS. (realFlowControl) diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index 00f63d475032..b19c3327419d 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -146,7 +146,7 @@ static HashTable dom_xpath_prop_handlers; static zend_object *dom_objects_namespace_node_new(zend_class_entry *class_type); static void dom_object_namespace_node_free_storage(zend_object *object); -static xmlNodePtr php_dom_create_fake_namespace_decl_node_ptr(xmlNodePtr nodep, xmlNsPtr original); +static xmlNodePtr php_dom_create_fake_namespace_decl_node_ptr(xmlNodePtr nodep, xmlNsPtr original, xmlDocPtr fallback_doc); typedef zend_result (*dom_read_t)(dom_object *obj, zval *retval); typedef zend_result (*dom_write_t)(dom_object *obj, zval *newval); @@ -705,7 +705,8 @@ static zend_object *dom_object_namespace_node_clone_obj(zend_object *zobject) xmlNodePtr original_node = dom_object_get_node(&intern->dom); if (original_node != NULL) { ZEND_ASSERT(original_node->type == XML_NAMESPACE_DECL); - xmlNodePtr cloned_node = php_dom_create_fake_namespace_decl_node_ptr(original_node->parent, original_node->ns); + xmlNodePtr parent = intern->parent_intern ? dom_object_get_node(intern->parent_intern) : NULL; + xmlNodePtr cloned_node = php_dom_create_fake_namespace_decl_node_ptr(parent, original_node->ns, original_node->doc); dom_update_refcount_after_clone(&intern->dom, original_node, &clone_intern->dom, cloned_node); } @@ -2318,15 +2319,16 @@ xmlNsPtr dom_get_nsdecl(xmlNode *node, xmlChar *localName) { } /* }}} end dom_get_nsdecl */ -static xmlNodePtr php_dom_create_fake_namespace_decl_node_ptr(xmlNodePtr nodep, xmlNsPtr original) +static xmlNodePtr php_dom_create_fake_namespace_decl_node_ptr(xmlNodePtr nodep, xmlNsPtr original, xmlDocPtr fallback_doc) { xmlNodePtr attrp; + xmlDocPtr doc = nodep ? nodep->doc : fallback_doc; xmlNsPtr curns = xmlNewNs(NULL, original->href, NULL); if (original->prefix) { curns->prefix = xmlStrdup(original->prefix); - attrp = xmlNewDocNode(nodep->doc, NULL, BAD_CAST original->prefix, original->href); + attrp = xmlNewDocNode(doc, NULL, BAD_CAST original->prefix, original->href); } else { - attrp = xmlNewDocNode(nodep->doc, NULL, BAD_CAST "xmlns", original->href); + attrp = xmlNewDocNode(doc, NULL, BAD_CAST "xmlns", original->href); } attrp->type = XML_NAMESPACE_DECL; attrp->parent = nodep; @@ -2337,7 +2339,7 @@ static xmlNodePtr php_dom_create_fake_namespace_decl_node_ptr(xmlNodePtr nodep, /* Note: Assumes the additional lifetime was already added in the caller. */ xmlNodePtr php_dom_create_fake_namespace_decl(xmlNodePtr nodep, xmlNsPtr original, zval *return_value, dom_object *parent_intern) { - xmlNodePtr attrp = php_dom_create_fake_namespace_decl_node_ptr(nodep, original); + xmlNodePtr attrp = php_dom_create_fake_namespace_decl_node_ptr(nodep, original, NULL); php_dom_create_object(attrp, return_value, parent_intern); /* This object must exist, because we just created an object for it via php_dom_create_object(). */ php_dom_namespace_node_obj_from_obj(Z_OBJ_P(return_value))->parent_intern = parent_intern; diff --git a/ext/dom/tests/dom_namespacenode_clone_xinclude.phpt b/ext/dom/tests/dom_namespacenode_clone_xinclude.phpt new file mode 100644 index 000000000000..33efc0a73b44 --- /dev/null +++ b/ext/dom/tests/dom_namespacenode_clone_xinclude.phpt @@ -0,0 +1,44 @@ +--TEST-- +DOMNameSpaceNode clone after xinclude does not use a dangling parent +--EXTENSIONS-- +dom +--FILE-- +'); +$href = 'file:///' . ltrim(str_replace('\\', '/', $included), '/'); + +$doc = new DOMDocument(); +$doc->loadXML(' + + +'); + +$xpath = new DOMXPath($doc); +$xpath->registerNamespace('xi', 'http://www.w3.org/2001/XInclude'); +$xi = $xpath->query('//xi:include')->item(0); +$ns = $xpath->query('namespace::local', $xi)->item(0); + +$live = clone $ns; +echo "live clone: ", $live->nodeName, "\n"; +echo "live parent: ", $live->parentNode->nodeName, "\n"; + +$doc->xinclude(); + +$clone = clone $ns; +echo "after xinclude: ", $clone->nodeName, "\n"; +var_dump($clone->parentNode); +var_dump($clone->parentElement); +var_dump($clone->isConnected); +?> +--CLEAN-- + +--EXPECT-- +live clone: xmlns:local +live parent: xi:include +after xinclude: xmlns:local +NULL +NULL +bool(false) From fcdc4fbf858c91ddfaa5d4203ea279737a102a19 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 13 Aug 2026 10:35:46 -0400 Subject: [PATCH 2/6] Fix DOMXPath crash wrapping a foreign node after a nodeset callback dom_xpath_intern_for_doc walked xpath_callbacks.node_list with Z_DOMOBJ_P on every entry. That list also stores arrays (nodeset-mode callback arguments). Same-document results take the early return; a later php:function that returns a node from another document walks the leftover array first. Skip non-objects and recurse into arrays so the matching intern is found instead of type-confusing the array. Closes GH-23253 --- NEWS | 2 + .../tests/xpath_php_function_foreign_doc.phpt | 40 +++++++++++++++++++ ext/dom/xpath.c | 23 ++++++++++- 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 ext/dom/tests/xpath_php_function_foreign_doc.phpt diff --git a/NEWS b/NEWS index 1bac1989e7ed..baea08f757f5 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,8 @@ PHP NEWS - DOM: . Fixed a use-after-free when cloning a DOMNameSpaceNode after DOMDocument::xinclude(). (iliaal) + . Fixed a crash in DOMXPath when a php:function callback receives a nodeset + and a later callback returns a node from another document. (iliaal) - Opcache: . Fixed opcache.protect_memory race under ZTS. (realFlowControl) diff --git a/ext/dom/tests/xpath_php_function_foreign_doc.phpt b/ext/dom/tests/xpath_php_function_foreign_doc.phpt new file mode 100644 index 000000000000..4e4b2b2c2822 --- /dev/null +++ b/ext/dom/tests/xpath_php_function_foreign_doc.phpt @@ -0,0 +1,40 @@ +--TEST-- +DOMXPath: php:function nodeset args plus a foreign-document return must not treat arrays as DOM objects +--EXTENSIONS-- +dom +--FILE-- +loadXML('1'); +$doc2 = new DOMDocument(); +$doc2->loadXML('2'); + +$xp = new DOMXPath($doc1); +$xp->registerNamespace('php', 'http://php.net/xpath'); +$xp->registerPhpFunctions(); + +function uses_nodeset($nodes) { + return true; +} + +function foreign() { + global $doc2; + return $doc2->documentElement; +} + +$xp->query('//a[php:function("uses_nodeset", //a)]'); +$res = $xp->query('php:function("foreign")'); +echo "count: "; +var_dump($res->length); +$n = $res->item(0); +echo "name: "; +var_dump($n->nodeName); +echo "owner is doc2: "; +var_dump($n->ownerDocument === $doc2); +echo "done\n"; +?> +--EXPECT-- +count: int(1) +name: string(4) "root" +owner is doc2: bool(true) +done diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c index 199dc96af40e..4688ed7743eb 100644 --- a/ext/dom/xpath.c +++ b/ext/dom/xpath.c @@ -35,6 +35,25 @@ #ifdef LIBXML_XPATH_ENABLED +static dom_object *dom_xpath_intern_from_entry(zval *entry, xmlDocPtr doc) +{ + if (Z_TYPE_P(entry) == IS_OBJECT) { + dom_object *obj = Z_DOMOBJ_P(entry); + if (obj->document && obj->document->ptr == doc) { + return obj; + } + } else if (Z_TYPE_P(entry) == IS_ARRAY) { + zval *inner; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(entry), inner) { + dom_object *obj = dom_xpath_intern_from_entry(inner, doc); + if (obj) { + return obj; + } + } ZEND_HASH_FOREACH_END(); + } + return NULL; +} + static dom_object *dom_xpath_intern_for_doc(dom_xpath_object *xpath_obj, xmlDocPtr doc) { if (xpath_obj->dom.document && xpath_obj->dom.document->ptr == doc) { @@ -44,8 +63,8 @@ static dom_object *dom_xpath_intern_for_doc(dom_xpath_object *xpath_obj, xmlDocP if (node_list) { zval *entry; ZEND_HASH_PACKED_FOREACH_VAL(node_list, entry) { - dom_object *obj = Z_DOMOBJ_P(entry); - if (obj->document && obj->document->ptr == doc) { + dom_object *obj = dom_xpath_intern_from_entry(entry, doc); + if (obj) { return obj; } } ZEND_HASH_FOREACH_END(); From 70603b9465f52a33bc5bb51ed4580a9f5ed56b34 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 17 Aug 2026 06:55:19 -0400 Subject: [PATCH 3/6] Stop grapheme_strrev from using UBRK_DONE as a byte index (#23323) ubrk_previous() returns UBRK_DONE after the first boundary. The loop condition ran before that assignment, so the body treated -1 as an offset and wrote into the zend_string NUL. Break when the iterator is done, and NUL-terminate the result of zend_string_alloc. --- NEWS | 4 +++ ext/intl/grapheme/grapheme_string.cpp | 4 +++ ext/intl/tests/grapheme_strrev_ubrk_done.phpt | 25 +++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 ext/intl/tests/grapheme_strrev_ubrk_done.phpt diff --git a/NEWS b/NEWS index a0c4359f4a09..6550a7a9641d 100644 --- a/NEWS +++ b/NEWS @@ -29,6 +29,10 @@ PHP NEWS left busy for the next fetch, and rows delivered from a result another statement took over. (KentarouTakeda) +- Intl: + . Fixed grapheme_strrev() treating UBRK_DONE as a byte index and leaving + the result without a terminating NUL. (iliaal) + - Phar: . Fixed Phar archives being automatically detected when ".phar" only occurs in a directory name or is not a filename extension in an included file's diff --git a/ext/intl/grapheme/grapheme_string.cpp b/ext/intl/grapheme/grapheme_string.cpp index 5e614be6ae72..a1daae84db9c 100644 --- a/ext/intl/grapheme/grapheme_string.cpp +++ b/ext/intl/grapheme/grapheme_string.cpp @@ -1175,6 +1175,9 @@ U_CFUNC PHP_FUNCTION(grapheme_strrev) current = ZSTR_LEN(string); for (end = pstr; pos != UBRK_DONE; ) { pos = ubrk_previous(bi); + if (pos == UBRK_DONE) { + break; + } end_len = current - pos; for (int32_t j = 0; j < end_len; j++) { *p++ = *(pstr + pos + j); @@ -1182,6 +1185,7 @@ U_CFUNC PHP_FUNCTION(grapheme_strrev) current = pos; } ubrk_end: + ZSTR_VAL(ret)[ZSTR_LEN(ret)] = '\0'; RETVAL_NEW_STR(ret); ubrk_close(bi); close: diff --git a/ext/intl/tests/grapheme_strrev_ubrk_done.phpt b/ext/intl/tests/grapheme_strrev_ubrk_done.phpt new file mode 100644 index 000000000000..6f70f7cd1f7f --- /dev/null +++ b/ext/intl/tests/grapheme_strrev_ubrk_done.phpt @@ -0,0 +1,25 @@ +--TEST-- +grapheme_strrev() stops at UBRK_DONE instead of using it as a byte index +--EXTENSIONS-- +intl +--FILE-- + +--EXPECT-- +3 3 636261 +1 1 61 +9 9 e5baa7e4b88be59c9f +9 9 65747962006c6c756e From 38e8b232da8981790d8c3aa635cf09875a837e32 Mon Sep 17 00:00:00 2001 From: Calvin Buckley Date: Mon, 17 Aug 2026 13:21:20 -0300 Subject: [PATCH 4/6] configure.ac: Use 64-bit fibers on AIX without overriding triplet (#23284) AIX/PASE defaults to a powerpc-ibm-(aix|os400) triplet, even though the build may be 64-bit. This causes a problem with fibers, as the 32-bit assembly will be selected, causing a build failure. Detect this case and override the selected assembly ourselves. Tested on PASE, which with IBM's provided packages, defaults to a 64-bit build environment. --- configure.ac | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 3229e0dac040..b1ff51bcc2d1 100644 --- a/configure.ac +++ b/configure.ac @@ -1224,7 +1224,15 @@ AS_CASE([$host_cpu], AS_CASE([$host_os], [darwin*], [fiber_os="mac"], - [aix*|os400*], [fiber_os="aix"], + [aix*|os400*], [ + fiber_os="aix" + dnl AIX triplets start w/ powerpc- even though it may be 64-bit (as AIX + dnl has a multiple bitness world). Check if the build is 64-bit, or we + dnl end up using the wrong fibre asm (w/o forcing a powerpc64 triplet) + AS_IF([test "$ac_cv_sizeof_long" -eq 8], [ + fiber_cpu="ppc64" + ], []) + ], [freebsd*], [fiber_os="freebsd"], [midipix], [fiber_os="midipix"], [fiber_os="other"] From a846204d45ae69d5e8ee2ed9f1e7fd3e103f5cb8 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Tue, 18 Aug 2026 01:27:34 +0800 Subject: [PATCH 5/6] CI: fix flacky ext/mysqli/tests/protocol_stmt_row_fetch_data.phpt (#23330) The package got mixed together some time. --- ext/mysqli/tests/fake_server.inc | 36 +++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/ext/mysqli/tests/fake_server.inc b/ext/mysqli/tests/fake_server.inc index d6afbb049773..dad8bc52ddd1 100644 --- a/ext/mysqli/tests/fake_server.inc +++ b/ext/mysqli/tests/fake_server.inc @@ -560,6 +560,32 @@ class my_mysqli_fake_server_conn } } + private function read_bytes(int $length): string + { + $data = ''; + while (strlen($data) < $length) { + $chunk = fread($this->conn, $length - strlen($data)); + if ($chunk === false || $chunk === '') { + throw new RuntimeException("Failed to read $length bytes from the client"); + } + $data .= $chunk; + } + return $data; + } + + public function read_packets(int $packet_count): void + { + $data = ''; + for ($i = 0; $i < $packet_count; $i++) { + $header = $this->read_bytes(4); + $packet_length = ord($header[0]) + | (ord($header[1]) << 8) + | (ord($header[2]) << 16); + $data .= $header . $this->read_bytes($packet_length); + } + fprintf(STDERR, "[*] Received: %s\n", bin2hex($data)); + } + public function close() { fclose($this->conn); @@ -732,7 +758,7 @@ function my_mysqli_test_stmt_response_row_over_read_two_fields( $conn->packets_to_bytes($rh), "Malicious Stmt Response for data $field_name [Extract heap through buffer over-read]" ); - $conn->read(65536); + $conn->read_packets(2); } function my_mysqli_test_stmt_response_row_over_read_int(my_mysqli_fake_server_conn $conn): void @@ -778,15 +804,15 @@ function my_mysqli_test_stmt_response_row_over_read_bit(my_mysqli_fake_server_co function my_mysqli_test_stmt_response_row_read_two_fields(my_mysqli_fake_server_conn $conn): void { $conn->send_server_greetings(); - $conn->read(); + $conn->read_packets(1); $conn->send_server_ok(); - $conn->read(); + $conn->read_packets(1); $field_names = array_keys(my_mysqli_data_fields()); foreach ($field_names as $field_name) { $conn->send_server_stmt_prepare_data_response($field_name); - $conn->read(65536); + $conn->read_packets(1); $conn->send_server_stmt_execute_data_response($field_name); - $conn->read(65536); + $conn->read_packets(2); } } From 850aa02c143e3b6e91ac231d9c3e4bcea0239325 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Tue, 18 Aug 2026 02:40:56 +0800 Subject: [PATCH 6/6] Fix IntlGregorianCalendar double-free of an adopted TimeZone (#23321) The timezone-and-locale IntlGregorianCalendar constructor passes the TimeZone to an adopting ICU constructor. On failure, PHP deleted that zone again after the calendar destructor had already freed it. Closes #23321 --- NEWS | 4 ++++ ext/intl/calendar/gregoriancalendar_methods.cpp | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 338409dd70c5..8f462c66a26b 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,10 @@ PHP NEWS . Fixed a use-after-free when cloning a DOMNameSpaceNode after DOMDocument::xinclude(). (iliaal) +- Intl: + . Fixed a double-free when IntlGregorianCalendar construction fails after + the ICU constructor adopts the TimeZone. (iliaal) + - Opcache: . Fixed opcache.protect_memory race under ZTS. (realFlowControl) diff --git a/ext/intl/calendar/gregoriancalendar_methods.cpp b/ext/intl/calendar/gregoriancalendar_methods.cpp index 0b36e621ef7f..0725cb4582fd 100644 --- a/ext/intl/calendar/gregoriancalendar_methods.cpp +++ b/ext/intl/calendar/gregoriancalendar_methods.cpp @@ -168,7 +168,6 @@ static void _php_intlgregcal_constructor_body( if (gcal) { delete gcal; } - delete tz; if (!is_constructor) { zval_ptr_dtor(return_value); RETVAL_NULL();