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