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, &param->parameter);
+			zend_get_gc_buffer_add_zval(gc_buffer, &param->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, &param->parameter);
+			zend_get_gc_buffer_add_zval(gc_buffer, &param->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"
+}