Commit 6e871075758 for php.net

commit 6e871075758de4016e72913fbcef226ab6ea7ef1
Author: David Carlier <devnexen@gmail.com>
Date:   Sat May 2 05:35:29 2026 +0100

    ext/mysqlnd: Fix persistent free of non-persistent connect_attr key.

    set_client_option_2d() built the temporary key string with the
    connection's persistent flag but always released it with persistent=1.
    On a duplicate-key update of the connect_attr hash, zend_hash_update()
    does not retain the passed key, so the caller-owned non-persistent
    string was freed via free() instead of efree(), tripping the
    IS_STR_PERSISTENT assertion in debug builds and mismatching allocators
    in release. Reachable by retrying mysqli_real_connect() on a handle
    whose first connect failed, since mysqlnd re-adds _client_name and
    _server_host on every connect attempt.

    close GH-21931

diff --git a/NEWS b/NEWS
index 073a6a9acd4..eb0336f6293 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,9 @@ PHP                                                                        NEWS
 |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
 ?? ??? ????, PHP 8.4.22

+- MySQLnd:
+  . Fix persistent free of non-persistent connect_attr key (David Carlier).
+
 - Opcache:
   . Fixed tracing JIT crash when a VM interrupt is handled during an observed
     user function call. (Levi Morrison)
diff --git a/ext/mysqli/tests/mysqli_real_connect_retry_attr.phpt b/ext/mysqli/tests/mysqli_real_connect_retry_attr.phpt
new file mode 100644
index 00000000000..7f773180742
--- /dev/null
+++ b/ext/mysqli/tests/mysqli_real_connect_retry_attr.phpt
@@ -0,0 +1,26 @@
+--TEST--
+mysqli_real_connect() retry on same handle does not corrupt mysqlnd connect_attr
+--EXTENSIONS--
+mysqli
+--SKIPIF--
+<?php
+require_once 'skipifconnectfailure.inc';
+?>
+--FILE--
+<?php
+
+require 'connect.inc';
+
+$link = mysqli_init();
+@mysqli_real_connect($link, $host, $user, 'bogus_password_to_force_failure', $db, $port, $socket);
+
+if (!mysqli_real_connect($link, $host, $user, $passwd, $db, $port, $socket)) {
+    printf("[001] [%d] %s\n", mysqli_connect_errno(), mysqli_connect_error());
+}
+
+mysqli_close($link);
+
+print "done!";
+?>
+--EXPECT--
+done!
diff --git a/ext/mysqlnd/mysqlnd_connection.c b/ext/mysqlnd/mysqlnd_connection.c
index 2f09ac5ee83..2402ad6fc8f 100644
--- a/ext/mysqlnd/mysqlnd_connection.c
+++ b/ext/mysqlnd/mysqlnd_connection.c
@@ -1572,17 +1572,14 @@ MYSQLND_METHOD(mysqlnd_conn_data, set_client_option_2d)(MYSQLND_CONN_DATA * cons
 				zval attrz;
 				zend_string *str;

+				str = zend_string_init(key, strlen(key), conn->persistent);
+				ZVAL_NEW_STR(&attrz, zend_string_init(value, strlen(value), conn->persistent));
 				if (conn->persistent) {
-					str = zend_string_init(key, strlen(key), 1);
 					GC_MAKE_PERSISTENT_LOCAL(str);
-					ZVAL_NEW_STR(&attrz, zend_string_init(value, strlen(value), 1));
 					GC_MAKE_PERSISTENT_LOCAL(Z_COUNTED(attrz));
-				} else {
-					str = zend_string_init(key, strlen(key), 0);
-					ZVAL_NEW_STR(&attrz, zend_string_init(value, strlen(value), 0));
 				}
 				zend_hash_update(conn->options->connect_attr, str, &attrz);
-				zend_string_release_ex(str, 1);
+				zend_string_release_ex(str, conn->persistent);
 			}
 			break;
 		default: