Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 6 additions & 3 deletions ext/pdo/pdo_dbh.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
34 changes: 34 additions & 0 deletions ext/pdo_mysql/tests/persistent_liveness_evict.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
--TEST--
Persistent reconnect after a dead cached handle does not leak the old pdo_dbh_t
--EXTENSIONS--
pdo_mysql
--SKIPIF--
<?php
require_once __DIR__ . '/inc/mysql_pdo_test.inc';
MySQLPDOTest::skip();
?>
--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);
echo $next->query('SELECT 1')->fetchColumn(), "\n";
?>
--EXPECT--
1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So how does this test check for the leak?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It triggers the dead-handle reconnect. The leaked handle is pecalloc, so you only see it under valgrind or LSAN, not in --EXPECT--.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image Does it depend on some configuration data?

Loading