Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ PHP NEWS
. Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or
next() call on the inner generator). (iliaal)

- PDO:
. Fixed a use-after-free when bindValue()/execute()/closeCursor() is called
from a bound parameter's __toString() during execute(). (iliaal)

27 Aug 2026, PHP 8.4.25

- Core:
Expand Down
23 changes: 23 additions & 0 deletions ext/pdo/pdo_stmt.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ static inline bool rewrite_name_to_position(pdo_stmt_t *stmt, struct pdo_bound_p
}
/* }}} */

static bool pdo_stmt_disallow_reentrant_param_event(pdo_stmt_t *stmt)
{
if (UNEXPECTED(stmt->in_param_event)) {
zend_throw_error(NULL, "Cannot modify a PDOStatement while parameter hooks are running");
return false;
}
return true;
}

/* trigger callback hook for parameters */
static bool dispatch_param_event(pdo_stmt_t *stmt, enum pdo_param_event event_type) /* {{{ */
{
Expand All @@ -104,6 +113,7 @@ static bool dispatch_param_event(pdo_stmt_t *stmt, enum pdo_param_event event_ty
return 1;
}

stmt->in_param_event = 1;
ht = stmt->bound_params;

iterate:
Expand All @@ -121,6 +131,7 @@ static bool dispatch_param_event(pdo_stmt_t *stmt, enum pdo_param_event event_ty
goto iterate;
}

stmt->in_param_event = 0;
return ret;
}
/* }}} */
Expand Down Expand Up @@ -394,6 +405,9 @@ PHP_METHOD(PDOStatement, execute)
ZEND_PARSE_PARAMETERS_END();

PHP_STMT_GET_OBJ;
if (!pdo_stmt_disallow_reentrant_param_event(stmt)) {
RETURN_THROWS();
}
PDO_STMT_CLEAR_ERR();

if (input_params) {
Expand Down Expand Up @@ -1436,6 +1450,9 @@ static void register_bound_param(INTERNAL_FUNCTION_PARAMETERS, int is_param) /*
ZEND_PARSE_PARAMETERS_END();

PHP_STMT_GET_OBJ;
if (!pdo_stmt_disallow_reentrant_param_event(stmt)) {
RETURN_THROWS();
}

param.param_type = (int) param_type;

Expand Down Expand Up @@ -1485,6 +1502,9 @@ PHP_METHOD(PDOStatement, bindValue)
ZEND_PARSE_PARAMETERS_END();

PHP_STMT_GET_OBJ;
if (!pdo_stmt_disallow_reentrant_param_event(stmt)) {
RETURN_THROWS();
}
param.param_type = (int) param_type;

if (param.name) {
Expand Down Expand Up @@ -1930,6 +1950,9 @@ PHP_METHOD(PDOStatement, closeCursor)
ZEND_PARSE_PARAMETERS_NONE();

PHP_STMT_GET_OBJ;
if (!pdo_stmt_disallow_reentrant_param_event(stmt)) {
RETURN_THROWS();
}
if (!stmt->methods->cursor_closer) {
/* emulate it by fetching and discarding rows */
do {
Expand Down
3 changes: 2 additions & 1 deletion ext/pdo/php_pdo_driver.h
Original file line number Diff line number Diff line change
Expand Up @@ -567,8 +567,9 @@ struct _pdo_stmt_t {
* bindParam() for its prepared statements, if false, PDO should
* emulate prepare and bind on its behalf */
unsigned supports_placeholders:2;
unsigned in_param_event:1;

unsigned _reserved:29;
unsigned _reserved:28;

/* the number of columns in the result set; not valid until after
* the statement has been executed at least once. In some cases, might
Expand Down
102 changes: 102 additions & 0 deletions ext/pdo_sqlite/tests/pdo_sqlite_reentrant_bind.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
--TEST--
Rebinding or re-executing from a parameter __toString() must not mutate bound_params mid-FOREACH
--EXTENSIONS--
pdo_sqlite
--FILE--
<?php
class Rebind {
public function __construct(private PDOStatement $stmt) {}
public function __toString() {
try {
$this->stmt->bindValue(1, 'x');
echo "bindValue: no error\n";
} catch (Error $e) {
echo $e::class, ": ", $e->getMessage(), "\n";
}
return 'rebind';
}
}

class Reexec {
public function __construct(private PDOStatement $stmt) {}
public function __toString() {
try {
$this->stmt->execute(['x', 'y']);
echo "execute: no error\n";
} catch (Error $e) {
echo $e::class, ": ", $e->getMessage(), "\n";
}
return 'reexec';
}
}

class Reclose {
public function __construct(private PDOStatement $stmt) {}
public function __toString() {
try {
$this->stmt->closeCursor();
echo "closeCursor: no error\n";
} catch (Error $e) {
echo $e::class, ": ", $e->getMessage(), "\n";
}
return 'reclose';
}
}

$db = new PDO('sqlite::memory:');

echo "bindValue:\n";
$stmt = $db->prepare('SELECT ?, ?');
$p1 = 'placeholder';
$p2 = 'second';
$stmt->bindParam(1, $p1);
$stmt->bindParam(2, $p2);
$p1 = new Rebind($stmt);
try {
$stmt->execute();
echo "execute after bindValue: no error\n";
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), "\n";
}

echo "execute:\n";
$stmt = $db->prepare('SELECT ?, ?');
$p1 = 'placeholder';
$p2 = 'second';
$stmt->bindParam(1, $p1);
$stmt->bindParam(2, $p2);
$p1 = new Reexec($stmt);
try {
$stmt->execute();
echo "execute after execute: no error\n";
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), "\n";
}

echo "closeCursor:\n";
$stmt = $db->prepare('SELECT ?, ?');
$p1 = 'placeholder';
$p2 = 'second';
$stmt->bindParam(1, $p1);
$stmt->bindParam(2, $p2);
$p1 = new Reclose($stmt);
try {
$stmt->execute();
echo "execute after closeCursor: no error\n";
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), "\n";
}

echo "done\n";
?>
--EXPECT--
bindValue:
Error: Cannot modify a PDOStatement while parameter hooks are running
execute after bindValue: no error
execute:
Error: Cannot modify a PDOStatement while parameter hooks are running
execute after execute: no error
closeCursor:
Error: Cannot modify a PDOStatement while parameter hooks are running
execute after closeCursor: no error
done
Loading