Commit 6e6ec4b267e for php
commit 6e6ec4b267e728874098c1e7a9908130eb000c1a
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Fri Sep 25 00:03:04 2026 -0400
ext/pdo_pgsql: Clear the notice callback when a persistent handle is released
Pdo\Pgsql::setNoticeCallback() stores a request-allocated fcall cache on the
driver handle. A persistent handle outlives the request, so the next request
on the same connection uses freed memory as soon as it receives a notice or
sets a new callback. Drop the callback from the persistent_shutdown hook,
which pdo_sqlite also uses for its user functions, once the last object of
the request releases the handle.
Closes GH-25156
diff --git a/NEWS b/NEWS
index 309b14f9c04..24432565e6e 100644
--- a/NEWS
+++ b/NEWS
@@ -245,6 +245,11 @@ PHP NEWS
. Fixed bug GH-25006 (getColumnMeta() reads out of bounds for an
out-of-range column index). (Ilia Alshanetsky)
+- PDO_PGSQL:
+ . Fixed a use-after-free when a persistent connection receives a notice
+ after the request that called Pdo\Pgsql::setNoticeCallback() has ended.
+ (Ilia Alshanetsky)
+
- PDO_SQLite:
. Fixed bug GH-24990 (Statement unusable after a failed execute()). (lazerg)
. Fixed bug GH-25021 (SQLite createFunction corrupts large integers).
diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c
index 0c6175d365d..ad5d7d60b96 100644
--- a/ext/pdo_pgsql/pgsql_driver.c
+++ b/ext/pdo_pgsql/pgsql_driver.c
@@ -1297,6 +1297,15 @@ static const zend_function_entry *pdo_pgsql_get_driver_methods(pdo_dbh_t *dbh, i
}
}
+static void pdo_pgsql_request_shutdown(pdo_dbh_t *dbh)
+{
+ pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
+
+ if (H && dbh->refcount <= 2) {
+ pdo_pgsql_cleanup_notice_callback(H);
+ }
+}
+
static bool pdo_pgsql_set_attr(pdo_dbh_t *dbh, zend_long attr, zval *val)
{
bool bval;
@@ -1334,7 +1343,7 @@ static const struct pdo_dbh_methods pgsql_methods = {
pdo_pgsql_get_attribute,
pdo_pgsql_check_liveness, /* check_liveness */
pdo_pgsql_get_driver_methods, /* get_driver_methods */
- NULL,
+ pdo_pgsql_request_shutdown,
pgsql_handle_in_transaction,
NULL, /* get_gc */
pdo_pgsql_scanner
diff --git a/ext/pdo_pgsql/tests/pdopgsql_setNoticeCallback_persistent.phpt b/ext/pdo_pgsql/tests/pdopgsql_setNoticeCallback_persistent.phpt
new file mode 100644
index 00000000000..15c1d6d6943
--- /dev/null
+++ b/ext/pdo_pgsql/tests/pdopgsql_setNoticeCallback_persistent.phpt
@@ -0,0 +1,49 @@
+--TEST--
+Pdo\Pgsql::setNoticeCallback() does not outlive the request on a persistent connection
+--EXTENSIONS--
+pdo
+pdo_pgsql
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+require dirname(__DIR__, 2) . '/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--CONFLICTS--
+server
+--FILE--
+<?php
+require __DIR__ . '/config.inc';
+include __DIR__ . '/../../../sapi/cli/tests/php_cli_server.inc';
+
+$code = '$db = new Pdo\Pgsql(' . var_export($config['ENV']['PDOTEST_DSN'], true) . ', null, null, [PDO::ATTR_PERSISTENT => true]);' . <<<'PHP'
+if (isset($_GET['set'])) {
+ $db->setNoticeCallback(function ($message) {
+ echo trim($message), "\n";
+ });
+}
+$db->exec("DO $$ BEGIN RAISE NOTICE 'request {$_GET['n']}'; END $$");
+echo "request {$_GET['n']} done\n";
+PHP;
+
+$args = ['-d', 'extension_dir=' . ini_get('extension_dir')];
+foreach (['pdo', 'pdo_pgsql'] as $ext) {
+ $args[] = '-d';
+ $args[] = 'extension=' . (PHP_OS_FAMILY === 'Windows' ? 'php_' : '') . $ext . '.' . PHP_SHLIB_SUFFIX;
+}
+if (php_ini_loaded_file()) {
+ $args[] = '-c';
+ $args[] = php_ini_loaded_file();
+}
+php_cli_server_start($code, 'index.php', $args);
+
+echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/?n=1&set=1');
+echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/?n=2');
+echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/?n=3&set=1');
+?>
+--EXPECT--
+NOTICE: request 1
+request 1 done
+request 2 done
+NOTICE: request 3
+request 3 done