Commit 296e8eac578 for php
commit 296e8eac57883cfd54e84d80201ed7e5de9f7ee1
Author: David Carlier <devnexen@gmail.com>
Date: Thu Oct 22 12:05:20 2026 +0100
pdo_pgsql_request_shutdown() leaves statements of freed PDO objects prepared.
Track prepared statement names on the handle and close the leftovers.
Co-authored-by: Artem Ukrainskiy <art.ukrainskiy@gmail.com>
diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c
index 38d2044290d..548a47c3ee8 100644
--- a/ext/pdo_pgsql/pgsql_driver.c
+++ b/ext/pdo_pgsql/pgsql_driver.c
@@ -251,6 +251,11 @@ static void pgsql_handle_closer(pdo_dbh_t *dbh) /* {{{ */
pefree(H->lob_streams, dbh->is_persistent);
H->lob_streams = NULL;
}
+ if (H->prepared_stmts) {
+ zend_hash_destroy(H->prepared_stmts);
+ pefree(H->prepared_stmts, 1);
+ H->prepared_stmts = NULL;
+ }
pdo_pgsql_cleanup_notice_callback(H);
if (H->server) {
PQfinish(H->server);
@@ -1398,6 +1403,21 @@ static void pdo_pgsql_request_shutdown(pdo_dbh_t *dbh)
pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
if(H->server) {
+ zend_string *name;
+
+ ZEND_HASH_MAP_FOREACH_STR_KEY(H->prepared_stmts, name) {
+#ifndef HAVE_PQCLOSEPREPARED
+ char *q;
+ spprintf(&q, 0, "DEALLOCATE %s", ZSTR_VAL(name));
+ res = PQexec(H->server, q);
+ efree(q);
+#else
+ res = PQclosePrepared(H->server, ZSTR_VAL(name));
+#endif
+ PQclear(res);
+ } ZEND_HASH_FOREACH_END();
+ zend_hash_clean(H->prepared_stmts);
+
res = PQexec(H->server, "CLOSE ALL; SET SESSION AUTHORIZATION DEFAULT; RESET ALL; UNLISTEN *; "
"SELECT pg_advisory_unlock_all(); DISCARD TEMP; DISCARD SEQUENCES");
@@ -1525,6 +1545,10 @@ static int pdo_pgsql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{
H->server = PQconnectdb(ZSTR_VAL(conn_str.s));
H->lob_streams = (HashTable *) pemalloc(sizeof(HashTable), dbh->is_persistent);
zend_hash_init(H->lob_streams, 0, NULL, NULL, 1);
+ if (dbh->is_persistent) {
+ H->prepared_stmts = (HashTable *) pemalloc(sizeof(HashTable), 1);
+ zend_hash_init(H->prepared_stmts, 0, NULL, NULL, 1);
+ }
if (tmp_user) {
zend_string_release_ex(tmp_user, 0);
diff --git a/ext/pdo_pgsql/pgsql_statement.c b/ext/pdo_pgsql/pgsql_statement.c
index 2f072553493..de7d03bc726 100644
--- a/ext/pdo_pgsql/pgsql_statement.c
+++ b/ext/pdo_pgsql/pgsql_statement.c
@@ -190,9 +190,11 @@ static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode)
res = PQclosePrepared(H->server, S->stmt_name);
#endif
if (res) {
+ if (H->prepared_stmts && PQresultStatus(res) == PGRES_COMMAND_OK) {
+ zend_hash_str_del(H->prepared_stmts, S->stmt_name, strlen(S->stmt_name));
+ }
PQclear(res);
}
-
S->is_prepared = false;
}
}
@@ -339,6 +341,9 @@ static int pgsql_stmt_execute(pdo_stmt_t *stmt)
case PGRES_TUPLES_OK:
/* it worked */
S->is_prepared = true;
+ if (H->prepared_stmts) {
+ zend_hash_str_add_empty_element(H->prepared_stmts, S->stmt_name, strlen(S->stmt_name));
+ }
PQclear(S->result);
S->result = NULL;
break;
diff --git a/ext/pdo_pgsql/php_pdo_pgsql_int.h b/ext/pdo_pgsql/php_pdo_pgsql_int.h
index dc71d5ae7a5..9ca729cd44a 100644
--- a/ext/pdo_pgsql/php_pdo_pgsql_int.h
+++ b/ext/pdo_pgsql/php_pdo_pgsql_int.h
@@ -45,6 +45,7 @@ typedef struct {
bool emulate_prepares;
bool disable_prepares;
HashTable *lob_streams;
+ HashTable *prepared_stmts;
zend_fcall_info_cache *notice_callback;
bool default_fetching_laziness;
zend_long default_chunk_size;
diff --git a/ext/pdo_pgsql/tests/gh24461_aborted_transaction_close.phpt b/ext/pdo_pgsql/tests/gh24461_aborted_transaction_close.phpt
new file mode 100644
index 00000000000..9c07b739b9b
--- /dev/null
+++ b/ext/pdo_pgsql/tests/gh24461_aborted_transaction_close.phpt
@@ -0,0 +1,40 @@
+--TEST--
+GH-24461 (Persistent connections: statements closed inside an aborted transaction are closed on reset)
+--EXTENSIONS--
+pdo_pgsql
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+putenv('PDOTEST_ATTR='.serialize([PDO::ATTR_PERSISTENT => true, PDO::ATTR_EMULATE_PREPARES => false]));
+
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+
+$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt');
+$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+$pid1 = (int) $pdo->query('SELECT pg_backend_pid()')->fetchColumn();
+$pdo->beginTransaction();
+$stmt = $pdo->prepare("SELECT 'gh24461_aborted'");
+$stmt->execute();
+try {
+ $pdo->exec('SELECT 1/0');
+} catch (PDOException $e) {
+}
+$stmt = null;
+$pdo->rollBack();
+$pdo = null;
+
+$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt');
+$pid2 = (int) $pdo->query('SELECT pg_backend_pid()')->fetchColumn();
+var_dump($pid1 === $pid2);
+var_dump($pdo->query("SELECT count(*) FROM pg_prepared_statements WHERE statement = 'SELECT ''gh24461_aborted'''")->fetchColumn());
+?>
+--CREDITS--
+Artem Ukrainskiy
+--EXPECT--
+bool(true)
+string(1) "0"
diff --git a/ext/pdo_pgsql/tests/gh24461_leftover_statement.phpt b/ext/pdo_pgsql/tests/gh24461_leftover_statement.phpt
new file mode 100644
index 00000000000..a418e3ad358
--- /dev/null
+++ b/ext/pdo_pgsql/tests/gh24461_leftover_statement.phpt
@@ -0,0 +1,37 @@
+--TEST--
+GH-24461 (Persistent connections: statements outliving their PDO object are closed on reset)
+--EXTENSIONS--
+pdo_pgsql
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+putenv('PDOTEST_ATTR='.serialize([PDO::ATTR_PERSISTENT => true]));
+
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+
+class HoldingPDO extends PDO {
+ public $stmt;
+}
+
+$pdo1 = PDOTest::test_factory(__DIR__ . '/common.phpt', HoldingPDO::class);
+$pid1 = (int) $pdo1->query('SELECT pg_backend_pid()')->fetchColumn();
+$pdo1->stmt = $pdo1->prepare("SELECT 'gh24461'");
+$pdo1->stmt->execute();
+
+unset($pdo1);
+var_dump(gc_collect_cycles() > 0);
+
+$pdo2 = PDOTest::test_factory(__DIR__ . '/common.phpt');
+$pid2 = (int) $pdo2->query('SELECT pg_backend_pid()')->fetchColumn();
+var_dump($pid1 === $pid2);
+var_dump($pdo2->query("SELECT count(*) FROM pg_prepared_statements WHERE statement = 'SELECT ''gh24461'''")->fetchColumn());
+?>
+--EXPECT--
+bool(true)
+bool(true)
+string(1) "0"