From 388c230352b5796e4a7f83f0222a81bcb4925e78 Mon Sep 17 00:00:00 2001 From: lazerg Date: Thu, 24 Sep 2026 18:32:26 +0500 Subject: [PATCH 1/4] Fix GH-23887: HTMLCollection::namedItem() mishandles the first child dom_html_collection_named_item() seeded the iterator with basep->children, but the handlers disagreed on what that seed meant: the noop handler never moved it, the class name handler skipped the first child, and the children handler never advanced past a non-matching first element. The iterator now starts with a NULL candidate, and the children and tag name handlers start from basep->children when they see NULL. Close GH-23888 --- NEWS | 2 ++ ext/dom/html_collection.c | 1 - ext/dom/obj_map.c | 7 ++--- ext/dom/tests/modern/html/gh23887.phpt | 39 ++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 ext/dom/tests/modern/html/gh23887.phpt diff --git a/NEWS b/NEWS index f0e89aa088bb..12b499085d86 100644 --- a/NEWS +++ b/NEWS @@ -27,6 +27,8 @@ PHP NEWS . Fixed Dom\HTMLDocument giving attributes the namespace of their element when a fragment is parsed with an xlink, xml or xmlns context element. (Ilia Alshanetsky) + . Fixed bug GH-23887 (Dom\HTMLCollection::namedItem() assertion failure, + hang, or missed first element). (Lazizbek Ergashev) - FTP: . Fixed bug GH-23619 (cryptic error on servers that don't support TLS diff --git a/ext/dom/html_collection.c b/ext/dom/html_collection.c index 2f156de2532e..a3c2b16f15be 100644 --- a/ext/dom/html_collection.c +++ b/ext/dom/html_collection.c @@ -48,7 +48,6 @@ static dom_named_item dom_html_collection_named_item(zend_string *key, zend_obje xmlNodePtr basep = dom_object_get_node(objmap->baseobj); if (basep != NULL && basep->children != NULL) { php_dom_obj_map_collection_iter iter = {0}; - iter.candidate = basep->children; iter.basep = basep; while (true) { diff --git a/ext/dom/obj_map.c b/ext/dom/obj_map.c index 88d7075be531..275c9e9930ee 100644 --- a/ext/dom/obj_map.c +++ b/ext/dom/obj_map.c @@ -311,9 +311,7 @@ static void dom_map_get_elements_item(dom_nnodemap_object *map, zend_long index, static void dom_map_collection_named_item_elements_iter(dom_nnodemap_object *map, php_dom_obj_map_collection_iter *iter) { - if (iter->candidate != iter->basep->children) { - iter->candidate = iter->candidate->next; - } + iter->candidate = iter->candidate ? iter->candidate->next : iter->basep->children; while (iter->candidate && iter->candidate->type != XML_ELEMENT_NODE) { iter->candidate = iter->candidate->next; } @@ -369,7 +367,8 @@ static void dom_map_get_by_class_name_item(dom_nnodemap_object *map, zend_long i static void dom_map_collection_named_item_by_tag_name_iter(dom_nnodemap_object *map, php_dom_obj_map_collection_iter *iter) { - iter->candidate = dom_get_elements_by_tag_name_ns_raw(iter->basep, iter->candidate, map->ns, map->local, map->local_lower, &iter->cur, iter->next); + xmlNodePtr nodep = iter->candidate ? iter->candidate : iter->basep->children; + iter->candidate = dom_get_elements_by_tag_name_ns_raw(iter->basep, nodep, map->ns, map->local, map->local_lower, &iter->cur, iter->next); iter->next = iter->cur + 1; } diff --git a/ext/dom/tests/modern/html/gh23887.phpt b/ext/dom/tests/modern/html/gh23887.phpt new file mode 100644 index 000000000000..a61f9c0b6a3e --- /dev/null +++ b/ext/dom/tests/modern/html/gh23887.phpt @@ -0,0 +1,39 @@ +--TEST-- +GH-23887 (Dom\HTMLCollection::namedItem() assertion failure, hang, or missed first element) +--CREDITS-- +Lu Maltsis (lmaltsis) +--EXTENSIONS-- +dom +--FILE-- + + + +HTML); +var_dump($dom->getElementById('container')->getElementsByClassName('')->namedItem("here")); + +$dom = Dom\HTMLDocument::createFromString('

', LIBXML_NOERROR); +$c = $dom->getElementById('c'); + +var_dump($c->getElementsByClassName('')->namedItem("a")); +var_dump($c->getElementsByClassName('x')->namedItem("a")->id); +var_dump($c->getElementsByClassName('x')->namedItem("b")->id); +var_dump($c->children->namedItem("a")->id); +var_dump($c->children->namedItem("b")->id); +var_dump($c->children->namedItem("c")); +var_dump($c->getElementsByTagName('p')->namedItem("a")->id); +var_dump($c->getElementsByTagName('p')->namedItem("b")->id); + +?> +--EXPECT-- +NULL +NULL +string(1) "a" +string(1) "b" +string(1) "a" +string(1) "b" +NULL +string(1) "a" +string(1) "b" From ae9d9cd897f5139103fe4a362037c09c15209e65 Mon Sep 17 00:00:00 2001 From: Marcel Hernandez <1ma@users.noreply.github.com> Date: Fri, 25 Sep 2026 09:44:29 +0200 Subject: [PATCH 2/4] fpm: Use recommended order for OpenMetrics stat metadata (#23871) --- NEWS | 4 ++++ sapi/fpm/fpm/fpm_status.c | 24 ++++++++++++------------ sapi/fpm/tests/status.inc | 26 +++++++++++++------------- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/NEWS b/NEWS index 2aaa3c4c3876..b0f120744463 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.7.0alpha1 +- FPM: + . Use recommended TYPE, HELP order for OpenMetrics metadata. + (Marcel Hernandez) + - Intl: . Fixed Collator attribute and strength methods not rejecting an unconstructed Collator. (Ilia Alshanetsky) diff --git a/sapi/fpm/fpm/fpm_status.c b/sapi/fpm/fpm/fpm_status.c index ff6ef68f3949..4e92198b757d 100644 --- a/sapi/fpm/fpm/fpm_status.c +++ b/sapi/fpm/fpm/fpm_status.c @@ -380,44 +380,44 @@ int fpm_status_handle_request(void) /* {{{ */ time_format = "%s"; short_syntax = - "# HELP phpfpm_up Could pool %s using a %s PM on PHP-FPM be reached?\n" "# TYPE phpfpm_up gauge\n" + "# HELP phpfpm_up Could pool %s using a %s PM on PHP-FPM be reached?\n" "phpfpm_up 1\n" - "# HELP phpfpm_start_since The number of seconds since FPM has started.\n" "# TYPE phpfpm_start_since counter\n" + "# HELP phpfpm_start_since The number of seconds since FPM has started.\n" "phpfpm_start_since %lu\n" - "# HELP phpfpm_accepted_connections The number of requests accepted by the pool.\n" "# TYPE phpfpm_accepted_connections counter\n" + "# HELP phpfpm_accepted_connections The number of requests accepted by the pool.\n" "phpfpm_accepted_connections %lu\n" - "# HELP phpfpm_listen_queue The number of requests in the queue of pending connections.\n" "# TYPE phpfpm_listen_queue gauge\n" + "# HELP phpfpm_listen_queue The number of requests in the queue of pending connections.\n" "phpfpm_listen_queue %d\n" - "# HELP phpfpm_max_listen_queue The maximum number of requests in the queue of pending connections since FPM has started.\n" "# TYPE phpfpm_max_listen_queue counter\n" + "# HELP phpfpm_max_listen_queue The maximum number of requests in the queue of pending connections since FPM has started.\n" "phpfpm_max_listen_queue %d\n" "# TYPE phpfpm_listen_queue_length gauge\n" "# HELP phpfpm_listen_queue_length The size of the socket queue of pending connections.\n" "phpfpm_listen_queue_length %u\n" - "# HELP phpfpm_idle_processes The number of idle processes.\n" "# TYPE phpfpm_idle_processes gauge\n" + "# HELP phpfpm_idle_processes The number of idle processes.\n" "phpfpm_idle_processes %d\n" - "# HELP phpfpm_active_processes The number of active processes.\n" "# TYPE phpfpm_active_processes gauge\n" + "# HELP phpfpm_active_processes The number of active processes.\n" "phpfpm_active_processes %d\n" - "# HELP phpfpm_total_processes The number of idle + active processes.\n" "# TYPE phpfpm_total_processes gauge\n" + "# HELP phpfpm_total_processes The number of idle + active processes.\n" "phpfpm_total_processes %d\n" - "# HELP phpfpm_max_active_processes The maximum number of active processes since FPM has started.\n" "# TYPE phpfpm_max_active_processes counter\n" + "# HELP phpfpm_max_active_processes The maximum number of active processes since FPM has started.\n" "phpfpm_max_active_processes %d\n" - "# HELP phpfpm_max_children_reached The number of times, the process limit has been reached, when pm tries to start more children (works only for pm 'dynamic' and 'ondemand').\n" "# TYPE phpfpm_max_children_reached counter\n" + "# HELP phpfpm_max_children_reached The number of times, the process limit has been reached, when pm tries to start more children (works only for pm 'dynamic' and 'ondemand').\n" "phpfpm_max_children_reached %u\n" - "# HELP phpfpm_slow_requests The number of requests that exceeded your 'request_slowlog_timeout' value.\n" "# TYPE phpfpm_slow_requests counter\n" + "# HELP phpfpm_slow_requests The number of requests that exceeded your 'request_slowlog_timeout' value.\n" "phpfpm_slow_requests %lu\n" - "# HELP phpfpm_memory_peak The memory usage peak since FPM has started.\n" "# TYPE phpfpm_memory_peak gauge\n" + "# HELP phpfpm_memory_peak The memory usage peak since FPM has started.\n" "phpfpm_memory_peak %zu\n" "# EOF\n"; diff --git a/sapi/fpm/tests/status.inc b/sapi/fpm/tests/status.inc index 76ef7fd55e11..c5858d3f68af 100644 --- a/sapi/fpm/tests/status.inc +++ b/sapi/fpm/tests/status.inc @@ -231,44 +231,44 @@ class Status */ protected function checkStatusOpenmetrics(string $body, array $fields) { - $pattern = "(# HELP phpfpm_up Could pool " . $fields['pool'] . " using a " . $fields['process manager'] . " PM on PHP-FPM be reached\?\n" . - "# TYPE phpfpm_up gauge\n" . + $pattern = "(# TYPE phpfpm_up gauge\n" . + "# HELP phpfpm_up Could pool " . $fields['pool'] . " using a " . $fields['process manager'] . " PM on PHP-FPM be reached\?\n" . "phpfpm_up 1\n" . - "# HELP phpfpm_start_since The number of seconds since FPM has started\.\n" . "# TYPE phpfpm_start_since counter\n" . + "# HELP phpfpm_start_since The number of seconds since FPM has started\.\n" . "phpfpm_start_since " . $fields['start since'] . "\n" . - "# HELP phpfpm_accepted_connections The number of requests accepted by the pool\.\n" . "# TYPE phpfpm_accepted_connections counter\n" . + "# HELP phpfpm_accepted_connections The number of requests accepted by the pool\.\n" . "phpfpm_accepted_connections " . $fields['accepted conn'] . "\n" . - "# HELP phpfpm_listen_queue The number of requests in the queue of pending connections\.\n" . "# TYPE phpfpm_listen_queue gauge\n" . + "# HELP phpfpm_listen_queue The number of requests in the queue of pending connections\.\n" . "phpfpm_listen_queue " . $fields['listen queue'] . "\n" . - "# HELP phpfpm_max_listen_queue The maximum number of requests in the queue of pending connections since FPM has started\.\n" . "# TYPE phpfpm_max_listen_queue counter\n" . + "# HELP phpfpm_max_listen_queue The maximum number of requests in the queue of pending connections since FPM has started\.\n" . "phpfpm_max_listen_queue " . $fields['max listen queue'] . "\n" . "# TYPE phpfpm_listen_queue_length gauge\n" . "# HELP phpfpm_listen_queue_length The size of the socket queue of pending connections\.\n" . "phpfpm_listen_queue_length " . $fields['listen queue len'] . "\n" . - "# HELP phpfpm_idle_processes The number of idle processes\.\n" . "# TYPE phpfpm_idle_processes gauge\n" . + "# HELP phpfpm_idle_processes The number of idle processes\.\n" . "phpfpm_idle_processes " . $fields['idle processes'] . "\n" . - "# HELP phpfpm_active_processes The number of active processes\.\n" . "# TYPE phpfpm_active_processes gauge\n" . + "# HELP phpfpm_active_processes The number of active processes\.\n" . "phpfpm_active_processes " . $fields['active processes'] . "\n" . - "# HELP phpfpm_total_processes The number of idle \+ active processes\.\n" . "# TYPE phpfpm_total_processes gauge\n" . + "# HELP phpfpm_total_processes The number of idle \+ active processes\.\n" . "phpfpm_total_processes " . $fields['total processes'] . "\n" . - "# HELP phpfpm_max_active_processes The maximum number of active processes since FPM has started\.\n" . "# TYPE phpfpm_max_active_processes counter\n" . + "# HELP phpfpm_max_active_processes The maximum number of active processes since FPM has started\.\n" . "phpfpm_max_active_processes " . $fields['max active processes'] . "\n" . - "# HELP phpfpm_max_children_reached The number of times, the process limit has been reached, when pm tries to start more children \(works only for pm 'dynamic' and 'ondemand'\)\.\n" . "# TYPE phpfpm_max_children_reached counter\n" . + "# HELP phpfpm_max_children_reached The number of times, the process limit has been reached, when pm tries to start more children \(works only for pm 'dynamic' and 'ondemand'\)\.\n" . "phpfpm_max_children_reached " . $fields['max children reached'] . "\n" . - "# HELP phpfpm_slow_requests The number of requests that exceeded your 'request_slowlog_timeout' value\.\n" . "# TYPE phpfpm_slow_requests counter\n" . + "# HELP phpfpm_slow_requests The number of requests that exceeded your 'request_slowlog_timeout' value\.\n" . "phpfpm_slow_requests " . $fields['slow requests'] . "\n" . - "# HELP phpfpm_memory_peak The memory usage peak since FPM has started\.\n" . "# TYPE phpfpm_memory_peak gauge\n" . + "# HELP phpfpm_memory_peak The memory usage peak since FPM has started\.\n" . "phpfpm_memory_peak " . $fields['memory peak'] . "\n" . "# EOF)\n"; From fedc7cbbd854407316779ddf0d2c5b9f473ea383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Fri, 25 Sep 2026 10:17:36 +0200 Subject: [PATCH 3/4] zend_compile: Disable `array_map()` optimization for `strict_types=1` (#23889) Fixes php/php-src#23882. --- NEWS | 2 ++ Zend/zend_compile.c | 6 ++++ .../array_map_foreach_optimization_009.phpt | 35 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 ext/opcache/tests/array_map_foreach_optimization_009.phpt diff --git a/NEWS b/NEWS index 525f1c98a58b..bc56592c0c61 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,8 @@ PHP NEWS (ndossche) . Fixed bug GH-23628 (Tracing JIT reads undefined property slots of lazy proxy objects instead of forwarding to the real instance). (lisachenko) + . Fixed bug GH-23882 (array_map() optimization is incorrect for + strict_types=1). (timwolla) 24 Sep 2026, PHP 8.6.0RC2 diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 0ced55283868..bad60a5f1071 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -5324,6 +5324,12 @@ static zend_result zend_compile_func_clone(znode *result, const zend_ast_list *a static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *args, zend_string *lcname, uint32_t lineno) /* {{{ */ { + /* array_map() as an internal function calls the callback as if strict_types=0, + * this optimization is therefore not legal if strict_types=1. */ + if (CG(active_op_array)->fn_flags & ZEND_ACC_STRICT_TYPES) { + return FAILURE; + } + /* Bail out if we do not have exactly two parameters. */ if (args->children != 2) { return FAILURE; diff --git a/ext/opcache/tests/array_map_foreach_optimization_009.phpt b/ext/opcache/tests/array_map_foreach_optimization_009.phpt new file mode 100644 index 000000000000..a1552c44f793 --- /dev/null +++ b/ext/opcache/tests/array_map_foreach_optimization_009.phpt @@ -0,0 +1,35 @@ +--TEST-- +array_map(): foreach optimization requires strict_types=0 (GH-23882) +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.opt_debug_level=0x20000 +--FILE-- + +--EXPECTF-- +$_main: + ; (lines=%d, args=0, vars=%d, tmps=%d) + ; (after optimizer) + ; %s +0000 INIT_FCALL 1 %d string("var_dump") +0001 INIT_FCALL 2 %d string("array_map") +0002 INIT_FCALL 0 %d string("trim") +0003 T0 = CALLABLE_CONVERT %d +0004 SEND_VAL T0 1 +0005 SEND_VAL array(...) 2 +0006 T0 = DO_ICALL +0007 SEND_VAL T0 1 +0008 DO_ICALL +0009 RETURN int(1) +array(1) { + [0]=> + string(1) "1" +} From 342a7465b3c5fb888a34492cf4d3dd402e13f4f0 Mon Sep 17 00:00:00 2001 From: Mrmaxmeier Date: Mon, 24 Aug 2026 16:26:08 +0200 Subject: [PATCH 4/4] Fix call_stack buffer overflow in zend_analyze_calls() The call stack was sized as op_array->last / 2, on the assumption that every call needs at least an INIT and a DO_FCALL opcode. That assumption does not hold after the optimizer has removed the DO_FCALL opcodes as dead code, in which case nothing pops the stack again: ```php function test() { new A(new B(new C(new D(match ([]) { 1 => 2 })))); } ``` The match arm never matches, so everything behind the ZEND_MATCH_ERROR is removed and the optimized op_array is just four ZEND_NEWs followed by the ZEND_MATCH_ERROR. The buffer then holds two entries while four are pushed. Size the stack by op_array->last instead, which is the only safe upper bound once the pushes and pops are no longer guaranteed to be balanced. Assisted-By: Claude Closes GH-23454. --- NEWS | 1 + Zend/Optimizer/zend_call_graph.c | 5 ++++- .../tests/opt/call_graph_stack_overflow.phpt | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 ext/opcache/tests/opt/call_graph_stack_overflow.phpt diff --git a/NEWS b/NEWS index da8362ce1edb..1de00a09ded0 100644 --- a/NEWS +++ b/NEWS @@ -61,6 +61,7 @@ PHP NEWS loop-invariant addition). (Ilia Alshanetsky) . Fixed OSS-Fuzz #5674034779193344 (Read of uninitialized memory in is_cacheable_stream_path()). (ndossche) + . Fix zend_analyze_calls() call_stack buffer overrun. (Mrmaxmeier) - PDO: . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid diff --git a/Zend/Optimizer/zend_call_graph.c b/Zend/Optimizer/zend_call_graph.c index 8a2f8ea2a7e1..cbb4c906a97e 100644 --- a/Zend/Optimizer/zend_call_graph.c +++ b/Zend/Optimizer/zend_call_graph.c @@ -54,7 +54,10 @@ ZEND_API void zend_analyze_calls(zend_arena **arena, zend_script *script, uint32 ALLOCA_FLAG(use_heap); bool is_prototype; - call_stack = do_alloca((op_array->last / 2) * sizeof(zend_call_info*), use_heap); + // Note: Reserve one call stack slot per operation. Each opcode pushes at + // most one entry to the call stack, and (with dead code elimination) it's + // possible to never pop from the stack. + call_stack = do_alloca(op_array->last * sizeof(zend_call_info*), use_heap); call_info = NULL; while (opline != end) { switch (opline->opcode) { diff --git a/ext/opcache/tests/opt/call_graph_stack_overflow.phpt b/ext/opcache/tests/opt/call_graph_stack_overflow.phpt new file mode 100644 index 000000000000..2044daf06c8d --- /dev/null +++ b/ext/opcache/tests/opt/call_graph_stack_overflow.phpt @@ -0,0 +1,17 @@ +--TEST-- +zend_analyze_calls(): call_stack overflow when dead code elimination removed the DO_FCALLs +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=0x7FFEBFFF +--FILE-- + 2 })))); +} +echo "OK\n"; +?> +--EXPECT-- +OK