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(¶m->driver_params);
/* param->parameter is freed by hash dtor */
ZVAL_UNDEF(¶m->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(¶m.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