Commit 6501051883f for php.net

commit 6501051883f09db5a232d160161057ed4721d006
Author: David Carlier <devnexen@gmail.com>
Date:   Sat May 2 05:27:39 2026 +0100

    ext/mysqli: Fix stmt->query leak in mysqli_execute_query() validation errors.

    When MYSQLI_REPORT_INDEX is enabled, mysqli_execute_query() duplicates
    the query string into stmt->query. The two input_params validation
    error branches freed the MY_STMT wrapper directly without releasing
    stmt->query, leaking the duplicated string per failing call.

    close GH-21930

diff --git a/NEWS b/NEWS
index e9226c59a0f..5dac2613a0e 100644
--- a/NEWS
+++ b/NEWS
@@ -21,6 +21,10 @@ PHP                                                                        NEWS
     IntlCalendar::equals(), ::before(), ::after(), and ::isEquivalentTo().
     (Weilin Du)

+- mysqli:
+  . Fix stmt->query leak in mysqli_execute_query() validation errors.
+    (David Carlier)
+
 - Phar:
   . Fixed a bypass of the magic ".phar" directory protection in
     Phar::addEmptyDir() for paths starting with "/.phar", while allowing
diff --git a/ext/mysqli/mysqli_api.c b/ext/mysqli/mysqli_api.c
index 2bc33e4ad67..1325736ccf4 100644
--- a/ext/mysqli/mysqli_api.c
+++ b/ext/mysqli/mysqli_api.c
@@ -532,6 +532,10 @@ PHP_FUNCTION(mysqli_execute_query)
 		MYSQLND_PARAM_BIND	*params;

 		if (!zend_array_is_list(input_params)) {
+			if (stmt->query) {
+				efree(stmt->query);
+				stmt->query = NULL;
+			}
 			mysqli_stmt_close(stmt->stmt, false);
 			stmt->stmt = NULL;
 			efree(stmt);
@@ -542,6 +546,10 @@ PHP_FUNCTION(mysqli_execute_query)
 		hash_num_elements = zend_hash_num_elements(input_params);
 		param_count = mysql_stmt_param_count(stmt->stmt);
 		if (hash_num_elements != param_count) {
+			if (stmt->query) {
+				efree(stmt->query);
+				stmt->query = NULL;
+			}
 			mysqli_stmt_close(stmt->stmt, false);
 			stmt->stmt = NULL;
 			efree(stmt);
diff --git a/ext/mysqli/tests/mysqli_execute_query_leak.phpt b/ext/mysqli/tests/mysqli_execute_query_leak.phpt
new file mode 100644
index 00000000000..11f56877aec
--- /dev/null
+++ b/ext/mysqli/tests/mysqli_execute_query_leak.phpt
@@ -0,0 +1,37 @@
+--TEST--
+mysqli_execute_query() does not leak stmt->query on input_params validation errors with MYSQLI_REPORT_INDEX
+--EXTENSIONS--
+mysqli
+--SKIPIF--
+<?php
+require_once 'skipifconnectfailure.inc';
+?>
+--FILE--
+<?php
+
+require 'table.inc';
+
+mysqli_report(MYSQLI_REPORT_INDEX);
+
+try {
+    $link->execute_query('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?', ['foo', 42]);
+} catch (ValueError $e) {
+    echo '[001] '.$e->getMessage()."\n";
+}
+
+try {
+    $link->execute_query('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?', ['foo' => 42]);
+} catch (ValueError $e) {
+    echo '[002] '.$e->getMessage()."\n";
+}
+
+print "done!";
+?>
+--CLEAN--
+<?php
+require_once 'clean_table.inc';
+?>
+--EXPECT--
+[001] mysqli::execute_query(): Argument #2 ($params) must consist of exactly 3 elements, 2 present
+[002] mysqli::execute_query(): Argument #2 ($params) must be a list array
+done!