Commit 9a3f37872ba for php
commit 9a3f37872bae87312f29333c4ccd923152dedb33
Author: Kamil Tekiela <tekiela246@gmail.com>
Date: Wed Apr 15 15:52:44 2026 +0100
Fix memory leak when closing a statement on a killed connection
Closes GH-21765
diff --git a/NEWS b/NEWS
index 7424a378c89..786f1fb0e12 100644
--- a/NEWS
+++ b/NEWS
@@ -70,6 +70,8 @@ PHP NEWS
- MySQLnd:
. Fixed field_count not resetting on OK packet. (Kamil Tekiela)
+ . Fixed memory leak when closing a prepared statement after its connection
+ was killed. (Kamil Tekiela)
- Opcache:
. Fixed OSS-Fuzz #546798343 (Heap-buffer-overflow in optimizer with
diff --git a/ext/mysqli/mysqli.c b/ext/mysqli/mysqli.c
index 987183aa9dc..5027a8dcd9f 100644
--- a/ext/mysqli/mysqli.c
+++ b/ext/mysqli/mysqli.c
@@ -116,7 +116,6 @@ void php_clear_stmt_bind(MY_STMT *stmt)
if (stmt->stmt) {
if (mysqli_stmt_close(stmt->stmt, true)) {
php_error_docref(NULL, E_WARNING, "Error occurred while closing statement");
- return;
}
}
diff --git a/ext/mysqli/tests/mysqli_stmt_close_killed_connection.phpt b/ext/mysqli/tests/mysqli_stmt_close_killed_connection.phpt
new file mode 100644
index 00000000000..8b573a7666c
--- /dev/null
+++ b/ext/mysqli/tests/mysqli_stmt_close_killed_connection.phpt
@@ -0,0 +1,32 @@
+--TEST--
+Closing a prepared statement after its connection was killed must not leak
+--EXTENSIONS--
+mysqli
+--SKIPIF--
+<?php
+require_once 'skipifconnectfailure.inc';
+?>
+--FILE--
+<?php
+require_once 'connect.inc';
+
+mysqli_report(MYSQLI_REPORT_OFF);
+$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket);
+
+$stmts = [];
+for ($i = 0; $i < 20; $i++) {
+ $stmts[] = $link->prepare('DO 1');
+}
+$link->query('KILL ' . $link->thread_id);
+
+// Over TCP, a few COM_STMT_CLOSE writes may be accepted before one fails
+do {
+ array_pop($stmts);
+ usleep(1000);
+} while ($stmts && !$link->errno);
+
+echo "done!\n";
+?>
+--EXPECTF--
+Warning: main(): Error occurred while closing statement in %s on line %d
+done!
diff --git a/ext/mysqlnd/mysqlnd_ps.c b/ext/mysqlnd/mysqlnd_ps.c
index 681c8f2a460..9254c58e58d 100644
--- a/ext/mysqlnd/mysqlnd_ps.c
+++ b/ext/mysqlnd/mysqlnd_ps.c
@@ -1758,6 +1758,7 @@ MYSQLND_METHOD_PRIVATE(mysqlnd_stmt, close_on_server)(MYSQLND_STMT * const s, bo
MYSQLND_STMT_DATA * stmt = s? s->data : NULL;
MYSQLND_CONN_DATA * conn = stmt? stmt->conn : NULL;
enum_mysqlnd_collected_stats statistic = STAT_LAST;
+ enum_func_status ret = PASS;
DBG_ENTER("mysqlnd_stmt::close_on_server");
if (!stmt || !conn) {
@@ -1795,14 +1796,7 @@ MYSQLND_METHOD_PRIVATE(mysqlnd_stmt, close_on_server)(MYSQLND_STMT * const s, bo
STAT_FREE_RESULT_EXPLICIT);
if (GET_CONNECTION_STATE(&conn->state) == CONN_READY) {
- enum_func_status ret = FAIL;
- const size_t stmt_id = stmt->stmt_id;
-
- ret = conn->command->stmt_close(conn, stmt_id);
- if (ret == FAIL) {
- COPY_CLIENT_ERROR(stmt->error_info, *conn->error_info);
- DBG_RETURN(FAIL);
- }
+ ret = conn->command->stmt_close(conn, stmt->stmt_id);
}
}
switch (stmt->execute_count) {
@@ -1831,7 +1825,7 @@ MYSQLND_METHOD_PRIVATE(mysqlnd_stmt, close_on_server)(MYSQLND_STMT * const s, bo
stmt->conn = NULL;
}
- DBG_RETURN(PASS);
+ DBG_RETURN(ret);
}
/* }}} */