Commit 71e56ed7a97 for php
commit 71e56ed7a977be45986755f2079cd6d3a9482626
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Tue Sep 29 11:31:33 2026 -0400
ext/pdo: Report bound params and columns to GC
PDOStatement's get_gc handler never reported bound_params or bound_columns,
so reference cycles through a bindParam()/bindColumn() variable or its
driver options stayed invisible to the collector and were never reclaimed.
Report the parameter and driver_params zvals of both tables.
Closes GH-23845
diff --git a/NEWS b/NEWS
index 4b9cd689113..1e1f3f6c8fe 100644
--- a/NEWS
+++ b/NEWS
@@ -135,6 +135,8 @@ PHP NEWS
"array given" regardless of the value passed. (Ilia Alshanetsky)
. Fixed PDOStatement::bindParam() and bindColumn() leaking the driver
options value. (Ilia Alshanetsky)
+ . Fixed PDOStatement not reporting its bound parameters and columns to the
+ cycle collector. (Ilia Alshanetsky)
- PDO_DBLIB:
. Fixed bug GH-23741 (segfault after a failed query inside a PDO
diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c
index bb998834048..9afdc52a229 100644
--- a/ext/pdo/pdo_stmt.c
+++ b/ext/pdo/pdo_stmt.c
@@ -2158,6 +2158,22 @@ static HashTable *dbstmt_get_gc(zend_object *object, zval **gc_data, int *gc_cou
zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create();
zend_get_gc_buffer_add_zval(gc_buffer, &stmt->database_object_handle);
zend_get_gc_buffer_add_zval(gc_buffer, &stmt->fetch.into);
+ if (stmt->bound_params) {
+ zval *val;
+ ZEND_HASH_FOREACH_VAL(stmt->bound_params, val) {
+ struct pdo_bound_param_data *param = Z_PTR_P(val);
+ zend_get_gc_buffer_add_zval(gc_buffer, ¶m->parameter);
+ zend_get_gc_buffer_add_zval(gc_buffer, ¶m->driver_params);
+ } ZEND_HASH_FOREACH_END();
+ }
+ if (stmt->bound_columns) {
+ zval *val;
+ ZEND_HASH_FOREACH_VAL(stmt->bound_columns, val) {
+ struct pdo_bound_param_data *param = Z_PTR_P(val);
+ zend_get_gc_buffer_add_zval(gc_buffer, ¶m->parameter);
+ zend_get_gc_buffer_add_zval(gc_buffer, ¶m->driver_params);
+ } ZEND_HASH_FOREACH_END();
+ }
zend_get_gc_buffer_use(gc_buffer, gc_data, gc_count);
/**
diff --git a/ext/pdo/tests/get_gc_bound_params.phpt b/ext/pdo/tests/get_gc_bound_params.phpt
new file mode 100644
index 00000000000..01c824a477c
--- /dev/null
+++ b/ext/pdo/tests/get_gc_bound_params.phpt
@@ -0,0 +1,66 @@
+--TEST--
+PDO Common: PDOStatement::get_gc() must report bound params and columns for cycle collection
+--EXTENSIONS--
+pdo
+--SKIPIF--
+<?php
+$dir = getenv('REDIR_TEST_DIR');
+if (false == $dir) die('skip no driver');
+require_once $dir . 'pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+if (getenv('REDIR_TEST_DIR') === false) putenv('REDIR_TEST_DIR='.__DIR__ . '/../../pdo/tests/');
+require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
+
+class Tracked {
+ public static array $collected = [];
+
+ public function __construct(public string $name, public PDOStatement $stmt) {}
+
+ public function __destruct() {
+ self::$collected[] = $this->name;
+ }
+}
+
+$db = PDOTest::factory();
+$db->exec('CREATE TABLE get_gc_bound_params (a INT, b INT)');
+
+$insert = $db->prepare('INSERT INTO get_gc_bound_params VALUES (?, ?)');
+$param = new Tracked('param', $insert);
+$insert->bindParam(1, $param, PDO::PARAM_INT);
+$paramOption = null;
+$insert->bindParam(2, $paramOption, PDO::PARAM_INT, 0, new Tracked('param driver option', $insert));
+
+$select = $db->query('SELECT a, b FROM get_gc_bound_params');
+$column = new Tracked('column', $select);
+$select->bindColumn(1, $column, PDO::PARAM_INT);
+$columnOption = null;
+$select->bindColumn(2, $columnOption, PDO::PARAM_INT, 0, new Tracked('column driver option', $select));
+
+unset($param, $paramOption, $column, $columnOption, $insert, $select, $db);
+var_dump(Tracked::$collected);
+gc_collect_cycles();
+sort(Tracked::$collected);
+var_dump(Tracked::$collected);
+?>
+--CLEAN--
+<?php
+require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
+$db = PDOTest::factory();
+PDOTest::dropTableIfExists($db, 'get_gc_bound_params');
+?>
+--EXPECT--
+array(0) {
+}
+array(4) {
+ [0]=>
+ string(6) "column"
+ [1]=>
+ string(20) "column driver option"
+ [2]=>
+ string(5) "param"
+ [3]=>
+ string(19) "param driver option"
+}