Commit 22237f2c07c for php

commit 22237f2c07c5260c2c0da7a5b002ac03ecb71473
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Sat Sep 26 18:15:07 2026 -0400

    ext/pdo: Release driver options after bindParam and bindColumn

    bindParam() and bindColumn() copied the driver options zval and then
    addref'd it again. The extra reference kept the value alive after the
    statement was destroyed. Keep the single reference from the copy.

    Closes GH-23936

diff --git a/NEWS b/NEWS
index 85b41dfb7ca..8eb6798b700 100644
--- a/NEWS
+++ b/NEWS
@@ -106,6 +106,8 @@ PHP                                                                        NEWS
     whose constructor arguments it rejects. (Ilia Alshanetsky)
   . Fixed PDO::ATTR_STATEMENT_CLASS constructor_args type errors reporting
     "array given" regardless of the value passed. (Ilia Alshanetsky)
+  . Fixed PDOStatement::bindParam() and bindColumn() leaking the driver
+    options value. (Ilia Alshanetsky)

 - PDO_Firebird:
   . Fixed bug GH-23758 (PDO_Firebird returns null for non-null empty BLOBs).
diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c
index 97d1a058fd5..c4abbfe4536 100644
--- a/ext/pdo/pdo_stmt.c
+++ b/ext/pdo/pdo_stmt.c
@@ -287,10 +287,6 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_
 	param->stmt = stmt;
 	param->is_param = is_param;

-	if (Z_REFCOUNTED(param->driver_params)) {
-		Z_ADDREF(param->driver_params);
-	}
-
 	if (!is_param && param->name && stmt->columns) {
 		/* try to map the name to the column */
 		int i;
@@ -374,6 +370,7 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_
 			} else {
 				zend_hash_index_del(hash, pparam->paramno);
 			}
+			ZVAL_UNDEF(&param->driver_params);
 			/* param->parameter is freed by hash dtor */
 			ZVAL_UNDEF(&param->parameter);
 			return 0;
@@ -1462,6 +1459,9 @@ static void register_bound_param(INTERNAL_FUNCTION_PARAMETERS, int is_param) /*
 		if (!Z_ISUNDEF(param.parameter)) {
 			zval_ptr_dtor(&(param.parameter));
 		}
+		if (!Z_ISUNDEF(param.driver_params)) {
+			zval_ptr_dtor(&param.driver_params);
+		}

 		RETURN_FALSE;
 	}
diff --git a/ext/pdo_sqlite/tests/pdo_driver_options_weakref.phpt b/ext/pdo_sqlite/tests/pdo_driver_options_weakref.phpt
new file mode 100644
index 00000000000..16dc9a3c466
--- /dev/null
+++ b/ext/pdo_sqlite/tests/pdo_driver_options_weakref.phpt
@@ -0,0 +1,35 @@
+--TEST--
+PDO SQLite releases driverOptions objects after binding or failed registration
+--EXTENSIONS--
+pdo_sqlite
+--FILE--
+<?php
+class Tracked {}
+
+$db = new PDO('sqlite::memory:');
+
+$stmt = $db->prepare('SELECT ? AS value');
+$value = 1;
+$driverOptions = [new Tracked()];
+$weakReference = WeakReference::create($driverOptions[0]);
+var_dump($stmt->bindParam(1, $value, PDO::PARAM_STR, 0, $driverOptions));
+unset($driverOptions, $value, $stmt);
+gc_collect_cycles();
+var_dump($weakReference->get());
+
+$stmt = $db->prepare('SELECT ? AS value');
+$stmt->execute();
+$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
+$value = null;
+$driverOptions = [new Tracked()];
+$weakReference = WeakReference::create($driverOptions[0]);
+var_dump(@$stmt->bindColumn('missing', $value, PDO::PARAM_STR, 0, $driverOptions));
+unset($driverOptions);
+var_dump($weakReference->get());
+unset($value, $stmt);
+?>
+--EXPECT--
+bool(true)
+NULL
+bool(false)
+NULL