From 1d0091e2619de15d76e868a12156e77bc1d79928 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 13 Aug 2026 09:13:00 -0400 Subject: [PATCH] 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. --- NEWS | 4 +++ ext/pdo/pdo_dbh.c | 9 +++-- .../tests/persistent_liveness_evict.phpt | 34 +++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 ext/pdo_mysql/tests/persistent_liveness_evict.phpt diff --git a/NEWS b/NEWS index 0d9e42792078..8c20ad7bc034 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,10 @@ PHP NEWS . Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or next() call on the inner generator). (iliaal) +- PDO: + . Fixed a leak when a persistent connection failed a liveness check + with no other live PDO handle. (iliaal) + 27 Aug 2026, PHP 8.4.25 - Core: 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..9c78c16f0f65 --- /dev/null +++ b/ext/pdo_mysql/tests/persistent_liveness_evict.phpt @@ -0,0 +1,34 @@ +--TEST-- +Persistent reconnect after a dead cached handle does not leak the old pdo_dbh_t +--EXTENSIONS-- +pdo_mysql +--SKIPIF-- + +--FILE-- + 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); +echo $next->query('SELECT 1')->fetchColumn(), "\n"; +?> +--EXPECT-- +1