Commit 17c806d8b34 for php
commit 17c806d8b341ca3c67fd56a3a9594c28a4cd5b51
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Sat Oct 24 19:25:22 2026 -0400
ext/pdo: Clear bindings when execute() fails to bind its array (#23970)
PDOStatement::execute() destroyed bound_params before registering the
entries of its array argument, so a failure part-way through (a value whose
string conversion throws) left the entries registered so far in place, and
a following execute() without arguments ran with them. Drop the partial
table on failure instead. really_register_bound_param() now looks the table
up only after converting the value, and raises the PDO_PARAM_EVT_ALLOC
error after undoing its insert, so a __toString() or error handler that
re-executes the statement no longer frees the table in use.
diff --git a/NEWS b/NEWS
index 6bb8496271f..efec2b84e88 100644
--- a/NEWS
+++ b/NEWS
@@ -87,6 +87,8 @@ PHP NEWS
that is not in the result set. (Ilia Alshanetsky)
. Fixed bug GH-23962 (Destroying a persistent PDO instance rolls back a
transaction still in use by another instance). (Lazizbek Ergashev)
+ . Fixed PDOStatement::execute() leaving a partial set of bindings in place
+ when binding its array argument fails. (Ilia Alshanetsky, Kamil Tekiela)
- PDO_PGSQL:
. Improved performance of the SQL parser that handles placeholders.
diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c
index 2a84d7ae2e9..7f39a2d5702 100644
--- a/ext/pdo/pdo_stmt.c
+++ b/ext/pdo/pdo_stmt.c
@@ -260,19 +260,6 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_
zval *parameter;
struct pdo_bound_param_data *pparam = NULL;
- hash = is_param ? stmt->bound_params : stmt->bound_columns;
-
- if (!hash) {
- ALLOC_HASHTABLE(hash);
- zend_hash_init(hash, 13, NULL, param_dtor, 0);
-
- if (is_param) {
- stmt->bound_params = hash;
- } else {
- stmt->bound_columns = hash;
- }
- }
-
if (!Z_ISREF(param->parameter)) {
parameter = ¶m->parameter;
} else {
@@ -348,6 +335,17 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_
/* delete any other parameter registered with this number.
* If the parameter is named, it will be removed and correctly
* disposed of by the hash_update call that follows */
+ hash = is_param ? stmt->bound_params : stmt->bound_columns;
+ if (!hash) {
+ ALLOC_HASHTABLE(hash);
+ zend_hash_init(hash, 13, NULL, param_dtor, 0);
+ if (is_param) {
+ stmt->bound_params = hash;
+ } else {
+ stmt->bound_columns = hash;
+ }
+ }
+
if (param->paramno >= 0) {
zend_hash_index_del(hash, param->paramno);
}
@@ -362,7 +360,6 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_
/* tell the driver we just created a parameter */
if (stmt->methods->param_hook) {
if (!stmt->methods->param_hook(stmt, pparam, PDO_PARAM_EVT_ALLOC)) {
- PDO_HANDLE_STMT_ERR();
/* undo storage allocation; the hash will free the parameter
* name if required */
if (pparam->name) {
@@ -373,6 +370,7 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_
ZVAL_UNDEF(¶m->driver_params);
/* param->parameter is freed by hash dtor */
ZVAL_UNDEF(¶m->parameter);
+ PDO_HANDLE_STMT_ERR();
return false;
}
}
@@ -434,6 +432,11 @@ PHP_METHOD(PDOStatement, execute)
if (!Z_ISUNDEF(param.parameter)) {
zval_ptr_dtor(¶m.parameter);
}
+ if (stmt->bound_params) {
+ zend_hash_destroy(stmt->bound_params);
+ FREE_HASHTABLE(stmt->bound_params);
+ stmt->bound_params = NULL;
+ }
RETURN_FALSE;
}
} ZEND_HASH_FOREACH_END();
diff --git a/ext/pdo/tests/pdo_bind_reentrant_execute.phpt b/ext/pdo/tests/pdo_bind_reentrant_execute.phpt
new file mode 100644
index 00000000000..dfc9fdab831
--- /dev/null
+++ b/ext/pdo/tests/pdo_bind_reentrant_execute.phpt
@@ -0,0 +1,42 @@
+--TEST--
+PDO: re-executing the statement while a bound value is converted to string
+--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 ReExecute
+{
+ public function __toString(): string
+ {
+ global $stmt;
+ $stmt->execute(['x', 'y']);
+ return 'r';
+ }
+}
+
+$db = PDOTest::factory();
+$db->exec('CREATE TABLE test_bind_reentrant (name varchar(10))');
+$stmt = $db->prepare('SELECT name FROM test_bind_reentrant WHERE name = ? OR name = ?');
+
+$stmt->execute(['a', new ReExecute()]);
+$stmt->bindValue(2, new ReExecute());
+echo "Done\n";
+?>
+--CLEAN--
+<?php
+require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
+$db = PDOTest::factory();
+PDOTest::dropTableIfExists($db, 'test_bind_reentrant');
+?>
+--EXPECT--
+Done
diff --git a/ext/pdo/tests/pdo_execute_array_binding_failure.phpt b/ext/pdo/tests/pdo_execute_array_binding_failure.phpt
new file mode 100644
index 00000000000..f247973874b
--- /dev/null
+++ b/ext/pdo/tests/pdo_execute_array_binding_failure.phpt
@@ -0,0 +1,67 @@
+--TEST--
+PDO: execute() leaves no bindings when binding its array argument fails
+--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 ThrowingString
+{
+ public function __toString(): string
+ {
+ throw new RuntimeException('conversion failed');
+ }
+}
+
+function bound_params_count(PDOStatement $stmt): string
+{
+ ob_start();
+ $stmt->debugDumpParams();
+ preg_match('/^Params:\s+(\d+)$/m', ob_get_clean(), $m);
+ return $m[1];
+}
+
+$db = PDOTest::factory();
+$db->exec('CREATE TABLE test_execute_bind_fail (id int, name varchar(10))');
+$db->exec("INSERT INTO test_execute_bind_fail (id, name) VALUES (1, 'a')");
+$db->exec("INSERT INTO test_execute_bind_fail (id, name) VALUES (2, 'b')");
+
+$stmt = $db->prepare('SELECT name FROM test_execute_bind_fail WHERE id = :id AND name = :name');
+$id = 1;
+$name = 'a';
+$stmt->bindParam(':id', $id);
+$stmt->bindParam(':name', $name);
+
+try {
+ $stmt->execute([':id' => 2, ':name' => new ThrowingString()]);
+} catch (RuntimeException $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+echo "bound params after failure: ", bound_params_count($stmt), PHP_EOL;
+
+var_dump($stmt->execute([':id' => 2, ':name' => 'b']));
+var_dump($stmt->fetchAll(PDO::FETCH_COLUMN));
+?>
+--CLEAN--
+<?php
+require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
+$db = PDOTest::factory();
+PDOTest::dropTableIfExists($db, 'test_execute_bind_fail');
+?>
+--EXPECT--
+RuntimeException: conversion failed
+bound params after failure: 0
+bool(true)
+array(1) {
+ [0]=>
+ string(1) "b"
+}