[PHP-CVS] [php-src] PHP-8.4: Fix leak when persistent PDO liveness check fails

[email protected] (Ilia Alshanetsky)
Newsgroups php.cvs
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-08-18T19:27:24-04:00

Commit: https://github.com/php/php-src/commit/b782a27242b49e182811bc23bce40ce3c77cd139
Raw diff: https://github.com/php/php-src/commit/b782a27242b49e182811bc23bce40ce3c77cd139.diff

Fix leak when persistent PDO liveness check fails

zend_list_close() only runs list_dtor_ex, which is NULL for PDO, so a
dead cached handle with refcount 1 was evicted and never freed.
Delete the persistent_list entry so php_pdo_pdbh_dtor runs. Live
handles (refcount > 1) still evict without freeing.

Closes GH-23249

Changed paths:
  A  ext/pdo_mysql/tests/persistent_liveness_evict.phpt
  M  NEWS
  M  ext/pdo/pdo_dbh.c


Diff:

diff --git a/NEWS b/NEWS
index d9f2e25bcdf4..7216f5c5a7d9 100644
--- a/NEWS
+++ b/NEWS
@@ -26,6 +26,10 @@ PHP                                                                        NEWS
 - Opcache:
   . Fixed opcache.protect_memory race under ZTS. (realFlowControl)
 
+- PDO:
+  . Fixed a leak when a persistent connection failed a liveness check
+    with no other live PDO handle. (iliaal)
+
 - Standard:
   . Fixed a memory leak in array_merge_recursive() when the recursive merge of
     an object converted to an array fails. (David Carlier)
diff --git a/ext/pdo/pdo_dbh.c b/ext/pdo/pdo_dbh.c
index 782639be0758..9f3d20745df9 100644
--- a/ext/pdo/pdo_dbh.c
+++ b/ext/pdo/pdo_dbh.c
@@ -411,9 +411,12 @@ PDO_API void php_pdo_internal_construct_driver(INTERNAL_FUNCTION_PARAMETERS, zen
 
 					/* is the connection still alive ? */
 					if (pdbh->methods->check_liveness && FAILURE == (pdbh->methods->check_liveness)(pdbh)) {
-						/* nope... need to kill it */
-						pdbh->refcount--;
-						zend_list_close(le);
+						if (pdbh->refcount > 1) {
+							pdbh->refcount--;
+							zend_list_close(le);
+						} else {
+							zend_hash_str_del(&EG(persistent_list), hashkey, plen);
+						}
 						pdbh = NULL;
 					}
 				}
diff --git a/ext/pdo_mysql/tests/persistent_liveness_evict.phpt b/ext/pdo_mysql/tests/persistent_liveness_evict.phpt
new file mode 100644
index 000000000000..545c16ebdbdb
--- /dev/null
+++ b/ext/pdo_mysql/tests/persistent_liveness_evict.phpt
@@ -0,0 +1,42 @@
+--TEST--
+Persistent reconnect after a dead cached handle does not leak the old pdo_dbh_t
+--EXTENSIONS--
+pdo_mysql
+--ENV--
+VALGRIND_OPTS=--leak-check=full
+--SKIPIF--
+<?php
+require_once __DIR__ . '/inc/mysql_pdo_test.inc';
+MySQLPDOTest::skip();
+if (!MySQLPDOTest::isPDOMySQLnd()) {
+    die('skip mysqlnd only (libmysql mysql_ping auto-reconnects)');
+}
+?>
+--FILE--
+<?php
+require_once __DIR__ . '/inc/mysql_pdo_test.inc';
+
+$dsn = MySQLPDOTest::getDSN();
+$user = PDO_MYSQL_TEST_USER;
+$pass = PDO_MYSQL_TEST_PASS;
+$opts = [
+    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+    PDO::ATTR_PERSISTENT => true,
+];
+
+$cached = new PDO($dsn, $user, $pass, $opts);
+$id = (int) $cached->query('SELECT CONNECTION_ID()')->fetchColumn();
+unset($cached);
+
+$killer = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
+$killer->exec('KILL ' . $id);
+unset($killer);
+
+$next = new PDO($dsn, $user, $pass, $opts);
+$nextId = (int) $next->query('SELECT CONNECTION_ID()')->fetchColumn();
+var_dump($nextId !== $id);
+echo $next->query('SELECT 1')->fetchColumn(), "\n";
+?>
+--EXPECT--
+bool(true)
+1
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.