Skip to content

Commit 75d1d56

Browse files
committed
ext/pdo: Clear bindings when execute() fails to bind its array
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.
1 parent 2e5bb79 commit 75d1d56

3 files changed

Lines changed: 86 additions & 14 deletions

File tree

‎NEWS‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ PHP NEWS
9898
that is not in the result set. (Ilia Alshanetsky)
9999
. Fixed bug GH-23962 (Destroying a persistent PDO instance rolls back a
100100
transaction still in use by another instance). (Lazizbek Ergashev)
101+
. Fixed PDOStatement::execute() leaving a partial set of bindings in place
102+
when binding its array argument fails. (Ilia Alshanetsky, Kamil Tekiela)
101103

102104
- Readline:
103105
. Fixed a heap over-read in the interactive shell prompt when cli.prompt is

‎ext/pdo/pdo_stmt.c‎

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -255,19 +255,6 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_
255255
zval *parameter;
256256
struct pdo_bound_param_data *pparam = NULL;
257257

258-
hash = is_param ? stmt->bound_params : stmt->bound_columns;
259-
260-
if (!hash) {
261-
ALLOC_HASHTABLE(hash);
262-
zend_hash_init(hash, 13, NULL, param_dtor, 0);
263-
264-
if (is_param) {
265-
stmt->bound_params = hash;
266-
} else {
267-
stmt->bound_columns = hash;
268-
}
269-
}
270-
271258
if (!Z_ISREF(param->parameter)) {
272259
parameter = &param->parameter;
273260
} else {
@@ -352,6 +339,17 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_
352339
/* delete any other parameter registered with this number.
353340
* If the parameter is named, it will be removed and correctly
354341
* disposed of by the hash_update call that follows */
342+
hash = is_param ? stmt->bound_params : stmt->bound_columns;
343+
if (!hash) {
344+
ALLOC_HASHTABLE(hash);
345+
zend_hash_init(hash, 13, NULL, param_dtor, 0);
346+
if (is_param) {
347+
stmt->bound_params = hash;
348+
} else {
349+
stmt->bound_columns = hash;
350+
}
351+
}
352+
355353
if (param->paramno >= 0) {
356354
zend_hash_index_del(hash, param->paramno);
357355
}
@@ -366,7 +364,6 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_
366364
/* tell the driver we just created a parameter */
367365
if (stmt->methods->param_hook) {
368366
if (!stmt->methods->param_hook(stmt, pparam, PDO_PARAM_EVT_ALLOC)) {
369-
PDO_HANDLE_STMT_ERR();
370367
/* undo storage allocation; the hash will free the parameter
371368
* name if required */
372369
if (pparam->name) {
@@ -376,6 +373,7 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_
376373
}
377374
/* param->parameter is freed by hash dtor */
378375
ZVAL_UNDEF(&param->parameter);
376+
PDO_HANDLE_STMT_ERR();
379377
return 0;
380378
}
381379
}
@@ -429,6 +427,11 @@ PHP_METHOD(PDOStatement, execute)
429427
if (!Z_ISUNDEF(param.parameter)) {
430428
zval_ptr_dtor(&param.parameter);
431429
}
430+
if (stmt->bound_params) {
431+
zend_hash_destroy(stmt->bound_params);
432+
FREE_HASHTABLE(stmt->bound_params);
433+
stmt->bound_params = NULL;
434+
}
432435
RETURN_FALSE;
433436
}
434437
} ZEND_HASH_FOREACH_END();
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
--TEST--
2+
PDO: execute() leaves no bindings when binding its array argument fails
3+
--EXTENSIONS--
4+
pdo
5+
--SKIPIF--
6+
<?php
7+
$dir = getenv('REDIR_TEST_DIR');
8+
if (false == $dir) die('skip no driver');
9+
require_once $dir . 'pdo_test.inc';
10+
PDOTest::skip();
11+
?>
12+
--FILE--
13+
<?php
14+
if (getenv('REDIR_TEST_DIR') === false) putenv('REDIR_TEST_DIR='.__DIR__ . '/../../pdo/tests/');
15+
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
16+
17+
class ThrowingString
18+
{
19+
public function __toString(): string
20+
{
21+
throw new RuntimeException('conversion failed');
22+
}
23+
}
24+
25+
function bound_params_count(PDOStatement $stmt): string
26+
{
27+
ob_start();
28+
$stmt->debugDumpParams();
29+
preg_match('/^Params:\s+(\d+)$/m', ob_get_clean(), $m);
30+
return $m[1];
31+
}
32+
33+
$db = PDOTest::factory();
34+
$db->exec('CREATE TABLE test_execute_bind_fail (id int, name varchar(10))');
35+
$db->exec("INSERT INTO test_execute_bind_fail (id, name) VALUES (1, 'a')");
36+
$db->exec("INSERT INTO test_execute_bind_fail (id, name) VALUES (2, 'b')");
37+
38+
$stmt = $db->prepare('SELECT name FROM test_execute_bind_fail WHERE id = :id AND name = :name');
39+
$id = 1;
40+
$name = 'a';
41+
$stmt->bindParam(':id', $id);
42+
$stmt->bindParam(':name', $name);
43+
44+
try {
45+
$stmt->execute([':id' => 2, ':name' => new ThrowingString()]);
46+
} catch (RuntimeException $e) {
47+
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
48+
}
49+
echo "bound params after failure: ", bound_params_count($stmt), PHP_EOL;
50+
51+
var_dump($stmt->execute([':id' => 2, ':name' => 'b']));
52+
var_dump($stmt->fetchAll(PDO::FETCH_COLUMN));
53+
?>
54+
--CLEAN--
55+
<?php
56+
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
57+
$db = PDOTest::factory();
58+
PDOTest::dropTableIfExists($db, 'test_execute_bind_fail');
59+
?>
60+
--EXPECT--
61+
RuntimeException: conversion failed
62+
bound params after failure: 0
63+
bool(true)
64+
array(1) {
65+
[0]=>
66+
string(1) "b"
67+
}

0 commit comments

Comments
 (0)