[php-src] master: Merge branch 'PHP-8.6'
| From: | Ilia Alshanetsky | Date: | Wed, 30 Sep 2026 18:20:15 +0000 |
| Subject: | [php-src] master: Merge branch 'PHP-8.6' | ||
| Groups: | php.cvs | ||
| Request: | Send a blank email to php-cvs+get-141818@lists.php.net to get a copy of this message | ||
Author: Ilia Alshanetsky (iliaal)
Date: 2026-09-30T13:56:15-04:00
Commit: https://github.com/php/php-src/commit/659272641aba394e61d8886fadfe8e424af9107b
Raw diff: https://github.com/php/php-src/commit/659272641aba394e61d8886fadfe8e424af9107b.diff
Merge branch 'PHP-8.6'
* PHP-8.6:
ext/pdo: Make PDOStatement::setFetchMode() updates atomic
Changed paths:
A ext/pdo/tests/pdo_setfetchmode_atomic.phpt
A ext/pdo/tests/pdo_setfetchmode_reentrant_fetch.phpt
A ext/pdo/tests/pdo_setfetchmode_reentrant_setmode.phpt
M ext/pdo/pdo_stmt.c
Diff:
diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c
index 7d25427a0d42..c828cc0bccaf 100644
--- a/ext/pdo/pdo_stmt.c
+++ b/ext/pdo/pdo_stmt.c
@@ -1660,20 +1660,21 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t
mode_a
uint32_t arg1_arg_num = mode_arg_num + 1;
uint32_t constructor_arg_num = mode_arg_num + 2;
uint32_t total_num_args = mode_arg_num + variadic_num_args;
-
- pdo_stmt_free_default_fetch_mode(stmt);
-
- stmt->default_fetch_type = stmt->dbh->default_fetch_type;
+ zend_long fetch_type = mode & ~PDO_FETCH_FLAGS;
+ zend_long fetch_column = 0;
+ zend_class_entry *fetch_class = NULL;
+ HashTable *fetch_ctor_args = NULL;
+ zend_object *fetch_into = NULL;
+ zend_object *old_into = NULL;
+ HashTable *old_ctor_args = NULL;
flags = mode & PDO_FETCH_FLAGS;
- if (!pdo_verify_fetch_mode(stmt->default_fetch_type, mode, mode_arg_num, false)) {
+ if (!pdo_verify_fetch_mode(stmt->dbh->default_fetch_type, mode, mode_arg_num, false)) {
return false;
}
- bool use_default = (mode & ~PDO_FETCH_FLAGS) == PDO_FETCH_USE_DEFAULT;
-
- switch (mode & ~PDO_FETCH_FLAGS) {
+ switch (fetch_type) {
case PDO_FETCH_USE_DEFAULT:
case PDO_FETCH_LAZY:
case PDO_FETCH_ASSOC:
@@ -1708,7 +1709,7 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t
mode_a
zend_argument_value_error(arg1_arg_num, "must be greater than or equal to 0");
return false;
}
- stmt->fetch.column = Z_LVAL(args[0]);
+ fetch_column = Z_LVAL(args[0]);
break;
case PDO_FETCH_CLASS: {
@@ -1759,11 +1760,10 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t
mode_a
zend_argument_value_error(3, "must be empty when class provided in argument #2 ($class)
does not have a constructor");
return false;
}
- GC_TRY_ADDREF(Z_ARRVAL(args[1]));
- stmt->fetch.cls.ctor_args = Z_ARRVAL(args[1]);
+ fetch_ctor_args = Z_ARRVAL(args[1]);
}
}
- stmt->fetch.cls.ce = cep;
+ fetch_class = cep;
}
break;
}
@@ -1780,18 +1780,52 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t
mode_a
return false;
}
- GC_ADDREF(Z_OBJ(args[0]));
- stmt->fetch.into = Z_OBJ(args[0]);
+ fetch_into = Z_OBJ(args[0]);
break;
default:
zend_argument_value_error(mode_arg_num, "must be one of the PDO::FETCH_* constants");
return false;
}
- if (!use_default) {
+ if ((stmt->default_fetch_type & ~PDO_FETCH_FLAGS) == PDO_FETCH_INTO) {
+ old_into = stmt->fetch.into;
+ } else if ((stmt->default_fetch_type & ~PDO_FETCH_FLAGS) == PDO_FETCH_CLASS) {
+ old_ctor_args = stmt->fetch.cls.ctor_args;
+ }
+ memset(&stmt->fetch, 0, sizeof(stmt->fetch));
+
+ switch (fetch_type) {
+ case PDO_FETCH_COLUMN:
+ stmt->fetch.column = fetch_column;
+ break;
+ case PDO_FETCH_CLASS:
+ stmt->fetch.cls.ce = fetch_class;
+ if (fetch_ctor_args) {
+ GC_TRY_ADDREF(fetch_ctor_args);
+ stmt->fetch.cls.ctor_args = fetch_ctor_args;
+ }
+ break;
+ case PDO_FETCH_INTO:
+ GC_ADDREF(fetch_into);
+ stmt->fetch.into = fetch_into;
+ break;
+ default:
+ break;
+ }
+
+ if (fetch_type == PDO_FETCH_USE_DEFAULT) {
+ stmt->default_fetch_type = stmt->dbh->default_fetch_type;
+ } else {
stmt->default_fetch_type = mode;
}
+ if (old_into) {
+ OBJ_RELEASE(old_into);
+ }
+ if (old_ctor_args) {
+ zend_array_release(old_ctor_args);
+ }
+
return true;
}
diff --git a/ext/pdo/tests/pdo_setfetchmode_atomic.phpt b/ext/pdo/tests/pdo_setfetchmode_atomic.phpt
new file mode 100644
index 000000000000..8751c27673a5
--- /dev/null
+++ b/ext/pdo/tests/pdo_setfetchmode_atomic.phpt
@@ -0,0 +1,73 @@
+--TEST--
+PDO Common: Failed setFetchMode must preserve the prior fetch mode and payload
+--EXTENSIONS--
+pdo
+--SKIPIF--
+<?php
+$dir = getenv('REDIR_TEST_DIR');
+if ($dir === false) {
+ 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';
+$pdo = PDOTest::factory();
+
+class FetchClass
+{
+ public int $i;
+
+ public function __construct(public string $marker)
+ {
+ }
+}
+
+$pdo->exec('CREATE TABLE pdo_setfetchmode_atomic (i INT)');
+$pdo->exec('INSERT INTO pdo_setfetchmode_atomic (i) VALUES (1)');
+$stmt = $pdo->query('SELECT i FROM pdo_setfetchmode_atomic');
+$row = new stdClass;
+$stmt->setFetchMode(PDO::FETCH_INTO, $row);
+try {
+ $stmt->setFetchMode(PDO::FETCH_CLASS);
+} catch (Throwable $e) {
+ echo "into error: ", $e::class, "\n";
+}
+$r = $stmt->fetch();
+
+echo "into object preserved: ";
+var_dump($r === $row);
+echo "into value: ", $row->i, "\n";
+
+$stmt = $pdo->query('SELECT i FROM pdo_setfetchmode_atomic');
+$stmt->setFetchMode(PDO::FETCH_CLASS, FetchClass::class, ['kept']);
+try {
+ $stmt->setFetchMode(PDO::FETCH_CLASS);
+} catch (Throwable $e) {
+ echo "class error: ", $e::class, "\n";
+}
+$r = $stmt->fetch();
+
+echo "class preserved: ", $r::class, "\n";
+echo "constructor argument: ", $r->marker, "\n";
+echo "class value: ", $r->i, "\n";
+?>
+--CLEAN--
+<?php
+require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
+$pdo = PDOTest::factory();
+PDOTest::dropTableIfExists($pdo, 'pdo_setfetchmode_atomic');
+?>
+--EXPECT--
+into error: ArgumentCountError
+into object preserved: bool(true)
+into value: 1
+class error: ArgumentCountError
+class preserved: FetchClass
+constructor argument: kept
+class value: 1
diff --git a/ext/pdo/tests/pdo_setfetchmode_reentrant_fetch.phpt
b/ext/pdo/tests/pdo_setfetchmode_reentrant_fetch.phpt
new file mode 100644
index 000000000000..bc163366fa8d
--- /dev/null
+++ b/ext/pdo/tests/pdo_setfetchmode_reentrant_fetch.phpt
@@ -0,0 +1,75 @@
+--TEST--
+PDO Common: setFetchMode autoload re-entry observes the prior complete mode
+--EXTENSIONS--
+pdo
+--SKIPIF--
+<?php
+$dir = getenv('REDIR_TEST_DIR');
+if ($dir === false) {
+ 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';
+$pdo = PDOTest::factory();
+
+class PriorFetchClass
+{
+ public int $i;
+
+ public function __construct(public string $marker)
+ {
+ }
+}
+
+$pdo->exec('CREATE TABLE pdo_setfetchmode_reentrant_fetch (i INT)');
+$pdo->exec('INSERT INTO pdo_setfetchmode_reentrant_fetch (i) VALUES (1)');
+$pdo->exec('INSERT INTO pdo_setfetchmode_reentrant_fetch (i) VALUES (2)');
+
+$stmt = $pdo->query('SELECT i FROM pdo_setfetchmode_reentrant_fetch ORDER BY i');
+$into = new stdClass;
+$stmt->setFetchMode(PDO::FETCH_INTO, $into);
+$autoload = function (string $class) use ($stmt, $into): void {
+ $row = $stmt->fetch();
+ echo "into re-entry preserved: ";
+ var_dump($row === $into);
+ eval("class $class { public int \$i; }");
+};
+spl_autoload_register($autoload);
+$stmt->setFetchMode(PDO::FETCH_CLASS, 'ReenteredIntoClass');
+spl_autoload_unregister($autoload);
+$row = $stmt->fetch();
+echo "into re-entry value: ", $into->i, "\n";
+echo "outer into class: ", $row::class, " ", $row->i, "\n";
+
+$stmt = $pdo->query('SELECT i FROM pdo_setfetchmode_reentrant_fetch ORDER BY i');
+$stmt->setFetchMode(PDO::FETCH_CLASS, PriorFetchClass::class, ['kept']);
+$autoload = function (string $class) use ($stmt): void {
+ $row = $stmt->fetch();
+ echo "class re-entry: ", $row::class, " ", $row->marker, " ",
$row->i, "\n";
+ eval("class $class { public int \$i; }");
+};
+spl_autoload_register($autoload);
+$stmt->setFetchMode(PDO::FETCH_CLASS, 'ReenteredFetchClass');
+spl_autoload_unregister($autoload);
+$row = $stmt->fetch();
+echo "outer class: ", $row::class, " ", $row->i, "\n";
+?>
+--CLEAN--
+<?php
+require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
+$pdo = PDOTest::factory();
+PDOTest::dropTableIfExists($pdo, 'pdo_setfetchmode_reentrant_fetch');
+?>
+--EXPECT--
+into re-entry preserved: bool(true)
+into re-entry value: 1
+outer into class: ReenteredIntoClass 2
+class re-entry: PriorFetchClass kept 1
+outer class: ReenteredFetchClass 2
diff --git a/ext/pdo/tests/pdo_setfetchmode_reentrant_setmode.phpt
b/ext/pdo/tests/pdo_setfetchmode_reentrant_setmode.phpt
new file mode 100644
index 000000000000..7eb68e10136d
--- /dev/null
+++ b/ext/pdo/tests/pdo_setfetchmode_reentrant_setmode.phpt
@@ -0,0 +1,53 @@
+--TEST--
+PDO Common: setFetchMode autoload re-entry may change the statement safely
+--EXTENSIONS--
+pdo
+--SKIPIF--
+<?php
+$dir = getenv('REDIR_TEST_DIR');
+if ($dir === false) {
+ 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';
+$pdo = PDOTest::factory();
+
+$pdo->exec('CREATE TABLE pdo_setfetchmode_reentrant_setmode (i INT)');
+$pdo->exec('INSERT INTO pdo_setfetchmode_reentrant_setmode (i) VALUES (1)');
+$stmt = $pdo->query('SELECT i FROM pdo_setfetchmode_reentrant_setmode');
+$first = new stdClass;
+$second = new stdClass;
+$stmt->setFetchMode(PDO::FETCH_INTO, $first);
+
+$autoload = function (string $class) use ($stmt, $second): void {
+ $stmt->setFetchMode(PDO::FETCH_INTO, $second);
+ eval("class $class { public int \$i; }");
+};
+spl_autoload_register($autoload);
+$stmt->setFetchMode(PDO::FETCH_CLASS, 'ReenteredSetModeClass');
+spl_autoload_unregister($autoload);
+
+$row = $stmt->fetch();
+echo $row::class, " ", $row->i, "\n";
+echo "first changed: ";
+var_dump(isset($first->i));
+echo "second changed: ";
+var_dump(isset($second->i));
+?>
+--CLEAN--
+<?php
+require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
+$pdo = PDOTest::factory();
+PDOTest::dropTableIfExists($pdo, 'pdo_setfetchmode_reentrant_setmode');
+?>
+--EXPECT--
+ReenteredSetModeClass 1
+first changed: bool(false)
+second changed: bool(false)