[php-src] master: Merge branch 'PHP-8.6'
| From: | Ilia Alshanetsky | Date: | Wed, 30 Sep 2026 12:55:12 +0000 |
| Subject: | [php-src] master: Merge branch 'PHP-8.6' | ||
| Groups: | php.cvs | ||
| Request: | Send a blank email to php-cvs+get-141783@lists.php.net to get a copy of this message | ||
Author: Ilia Alshanetsky (iliaal)
Date: 2026-09-30T08:51:00-04:00
Commit: https://github.com/php/php-src/commit/cb28aa03d51d4bb64c97709444923f0243595a1d
Raw diff: https://github.com/php/php-src/commit/cb28aa03d51d4bb64c97709444923f0243595a1d.diff
Merge branch 'PHP-8.6'
* PHP-8.6:
Fix HashTable UAF when rebound from a parameter __toString()
Changed paths:
A ext/pdo_sqlite/tests/pdo_sqlite_reentrant_bind.phpt
M ext/pdo/pdo_stmt.c
M ext/pdo/php_pdo_driver.h
Diff:
diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c
index 2dce9db46d17..758226957ce6 100644
--- a/ext/pdo/pdo_stmt.c
+++ b/ext/pdo/pdo_stmt.c
@@ -87,6 +87,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) /* {{{ */
{
@@ -102,6 +111,7 @@ static bool dispatch_param_event(pdo_stmt_t *stmt, enum pdo_param_event event_ty
return true;
}
+ stmt->in_param_event = 1;
ht = stmt->bound_params;
iterate:
@@ -119,6 +129,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;
}
/* }}} */
@@ -404,6 +415,9 @@ PHP_METHOD(PDOStatement, execute)
ZEND_PARSE_PARAMETERS_END();
PHP_STMT_GET_OBJ;
+ if (!pdo_stmt_disallow_reentrant_param_event(stmt)) {
+ RETURN_THROWS();
+ }
if (stmt->executed) {
pdo_stmt_invalidate_result(stmt);
@@ -1331,6 +1345,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;
@@ -1383,6 +1400,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) {
@@ -1844,6 +1864,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 {
diff --git a/ext/pdo/php_pdo_driver.h b/ext/pdo/php_pdo_driver.h
index 62e1f177ebb5..8fb7918e6973 100644
--- a/ext/pdo/php_pdo_driver.h
+++ b/ext/pdo/php_pdo_driver.h
@@ -573,7 +573,8 @@ struct _pdo_stmt_t {
* bindParam() for its prepared statements, if false, PDO should
* emulate prepare and bind on its behalf */
uint16_t supports_placeholders:2;
- uint16_t reserved: 12;
+ uint16_t in_param_event:1;
+ uint16_t reserved: 11;
/* keep track of bound input parameters. Some drivers support
* input/output parameters, but you can't rely on that working */
diff --git a/ext/pdo_sqlite/tests/pdo_sqlite_reentrant_bind.phpt
b/ext/pdo_sqlite/tests/pdo_sqlite_reentrant_bind.phpt
new file mode 100644
index 000000000000..3d2be7c7ed60
--- /dev/null
+++ b/ext/pdo_sqlite/tests/pdo_sqlite_reentrant_bind.phpt
@@ -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