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 @@ -2,6 +2,10 @@ PHP NEWS
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
?? ??? ????, PHP 8.4.25

- PDO:
. Fixed PDOStatement::setFetchMode() not applying changes atomically.
(iliaal)

- Date:
. Fixed leak on double DatePeriod::__construct() call. (ilutov)

Expand Down
80 changes: 56 additions & 24 deletions ext/pdo/pdo_stmt.c
Original file line number Diff line number Diff line change
Expand Up @@ -1717,27 +1717,24 @@ 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;
zend_long fetch_type = mode & ~PDO_FETCH_FLAGS;
zend_long fetch_column = 0;
zend_class_entry *fetch_class = NULL;
zend_array *fetch_ctor_args = NULL;
zend_object *fetch_into = NULL;
zval old_ctor_args;
zval old_into;

switch (stmt->default_fetch_type) {
case PDO_FETCH_INTO:
if (!Z_ISUNDEF(stmt->fetch.into)) {
zval_ptr_dtor(&stmt->fetch.into);
ZVAL_UNDEF(&stmt->fetch.into);
}
break;
default:
;
}

stmt->default_fetch_type = PDO_FETCH_BOTH;
ZVAL_UNDEF(&old_ctor_args);
ZVAL_UNDEF(&old_into);

flags = mode & PDO_FETCH_FLAGS;

if (!pdo_stmt_verify_mode(stmt, mode, mode_arg_num, false)) {
return false;
}

switch (mode & ~PDO_FETCH_FLAGS) {
switch (fetch_type) {
case PDO_FETCH_USE_DEFAULT:
case PDO_FETCH_LAZY:
case PDO_FETCH_ASSOC:
Expand Down Expand Up @@ -1772,13 +1769,11 @@ 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: {
HashTable *constructor_args = NULL;
/* Undef constructor arguments */
ZVAL_UNDEF(&stmt->fetch.cls.ctor_args);
/* Gets its class name from 1st column */
if ((flags & PDO_FETCH_CLASSTYPE) == PDO_FETCH_CLASSTYPE) {
if (variadic_num_args != 0) {
Expand All @@ -1788,7 +1783,6 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a
zend_string_release(func);
return false;
}
stmt->fetch.cls.ce = NULL;
} else {
zend_class_entry *cep;
if (variadic_num_args == 0) {
Expand Down Expand Up @@ -1827,15 +1821,17 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a
constructor_args = Z_ARRVAL(args[1]);
}
}
stmt->fetch.cls.ce = cep;
fetch_class = cep;

/* If constructor arguments are present and not empty */
if (constructor_args) {
ZVAL_ARR(&stmt->fetch.cls.ctor_args, zend_array_dup(constructor_args));
if (!cep->constructor) {
zend_throw_error(NULL, "User-supplied statement does not accept constructor arguments");
return false;
}
fetch_ctor_args = zend_array_dup(constructor_args);
}
}

do_fetch_class_prepare(stmt);
break;
}
case PDO_FETCH_INTO:
Expand All @@ -1851,15 +1847,53 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a
return false;
}

ZVAL_COPY(&stmt->fetch.into, &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 ((stmt->default_fetch_type & ~PDO_FETCH_FLAGS) == PDO_FETCH_INTO) {
ZVAL_COPY_VALUE(&old_into, &stmt->fetch.into);
ZVAL_UNDEF(&stmt->fetch.into);
} else if ((stmt->default_fetch_type & ~PDO_FETCH_FLAGS) == PDO_FETCH_CLASS) {
do_fetch_opt_finish(stmt, 0);
ZVAL_COPY_VALUE(&old_ctor_args, &stmt->fetch.cls.ctor_args);
ZVAL_UNDEF(&stmt->fetch.cls.ctor_args);
} else {
do_fetch_opt_finish(stmt, 1);
}

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) {
ZVAL_ARR(&stmt->fetch.cls.ctor_args, fetch_ctor_args);
} else {
ZVAL_UNDEF(&stmt->fetch.cls.ctor_args);
}
do_fetch_class_prepare(stmt);
break;
case PDO_FETCH_INTO:
ZVAL_OBJ_COPY(&stmt->fetch.into, fetch_into);
break;
default:
break;
}

stmt->default_fetch_type = mode;

if (!Z_ISUNDEF(old_into)) {
zval_ptr_dtor(&old_into);
}
if (!Z_ISUNDEF(old_ctor_args)) {
zval_ptr_dtor(&old_ctor_args);
}

return true;
}

Expand All @@ -1875,8 +1909,6 @@ PHP_METHOD(PDOStatement, setFetchMode)

PHP_STMT_GET_OBJ;

do_fetch_opt_finish(stmt, 1);

if (!pdo_stmt_setup_fetch_mode(stmt, fetch_mode, 1, args, num_args)) {
RETURN_THROWS();
}
Expand Down
73 changes: 73 additions & 0 deletions ext/pdo/tests/pdo_setfetchmode_atomic.phpt
Original file line number Diff line number Diff line change
@@ -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
75 changes: 75 additions & 0 deletions ext/pdo/tests/pdo_setfetchmode_reentrant_fetch.phpt
Original file line number Diff line number Diff line change
@@ -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
53 changes: 53 additions & 0 deletions ext/pdo/tests/pdo_setfetchmode_reentrant_setmode.phpt
Original file line number Diff line number Diff line change
@@ -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)
Loading