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
215 changes: 190 additions & 25 deletions ext/pdo/pdo_stmt.c

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion ext/pdo/php_pdo_driver.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_operation: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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,4 @@ $db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_fetch_class_change_ctor_five");
?>
--EXPECT--
PDOStatement::fetchAll(): The PDO::FETCH_SERIALIZE mode is deprecated
Error: Cannot change default fetch mode while fetching
Error: Cannot perform another operation on this PDOStatement while an operation is in progress
124 changes: 124 additions & 0 deletions ext/pdo/tests/pdo_stmt_reentrant_bind.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
--TEST--
PDO Common: PDOStatement rejects reentrant operations while changing parameter bindings
--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';
$db = PDOTest::factory();

function print_exception(Throwable $e): void
{
echo $e::class, ': ', $e->getMessage(), "\n";
}

final class ReenterOnString
{
public function __construct(
private PDOStatement $statement,
private PDOStatement $otherStatement,
) {}

public function __toString(): string
{
try {
$this->statement->execute([]);
} catch (Throwable $e) {
print_exception($e);
}

var_dump($this->otherStatement->execute());
$this->otherStatement->closeCursor();
return '1';
}
}

final class ReenterOnDestruct
{
private WeakReference $statement;

public function __construct(
PDOStatement $statement,
private string $operation,
) {
$this->statement = WeakReference::create($statement);
}

public function __destruct()
{
$statement = $this->statement->get();
try {
if ($this->operation === 'execute') {
$statement->execute([]);
} else {
$statement->bindValue(1, 1, PDO::PARAM_INT);
}
} catch (Throwable $e) {
print_exception($e);
}
}
}

$db->exec('CREATE TABLE pdo_reentrant_bind (val INT)');
$db->exec('INSERT INTO pdo_reentrant_bind VALUES (1)');

echo "conversion\n";
$statement = $db->prepare('SELECT val FROM pdo_reentrant_bind WHERE val = ?');
$otherStatement = $db->prepare('SELECT val FROM pdo_reentrant_bind');
$value = new ReenterOnString($statement, $otherStatement);
var_dump($statement->bindParam(1, $value, PDO::PARAM_STR));
var_dump($statement->execute());
var_dump($statement->fetchColumn());
$statement->closeCursor();

echo "replacement\n";
$statement = $db->prepare('SELECT val FROM pdo_reentrant_bind WHERE val = ?');
$statement->bindValue(1, new ReenterOnDestruct($statement, 'execute'), PDO::PARAM_NULL);
var_dump($statement->bindValue(1, 1, PDO::PARAM_INT));
var_dump($statement->execute());
$statement->closeCursor();

echo "teardown\n";
$statement = $db->prepare('SELECT val FROM pdo_reentrant_bind WHERE val = ?');
$statement->bindValue(1, new ReenterOnDestruct($statement, 'bind'), PDO::PARAM_NULL);
var_dump($statement->execute([1]));
$statement->closeCursor();
var_dump($statement->bindValue(1, 1, PDO::PARAM_INT));
var_dump($statement->execute());
var_dump($statement->fetchColumn());
?>
--CLEAN--
<?php
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, 'pdo_reentrant_bind');
?>
--EXPECT--
conversion
Error: Cannot perform another operation on this PDOStatement while an operation is in progress
bool(true)
bool(true)
bool(true)
string(1) "1"
replacement
Error: Cannot perform another operation on this PDOStatement while an operation is in progress
bool(true)
bool(true)
teardown
Error: Cannot perform another operation on this PDOStatement while an operation is in progress
bool(true)
bool(true)
bool(true)
string(1) "1"
59 changes: 59 additions & 0 deletions ext/pdo/tests/pdo_stmt_reentrant_debug_dump.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
--TEST--
PDO Common: PDOStatement rejects reentrant binding from a debug output handler
--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';
$db = PDOTest::factory();
$db->exec('CREATE TABLE pdo_reentrant_debug_dump (val INT)');
$db->exec('INSERT INTO pdo_reentrant_debug_dump VALUES (1)');
$statement = $db->prepare('SELECT val FROM pdo_reentrant_debug_dump WHERE val = ?');
$statement->bindValue(1, 1, PDO::PARAM_INT);

$called = false;
$messages = [];
$output = '';
ob_start(static function (string $buffer) use ($statement, &$called, &$messages, &$output): string {
$output .= $buffer;
if (!$called && str_contains($output, 'Key: Position')) {
$called = true;
try {
$statement->bindValue(1, 2, PDO::PARAM_INT);
} catch (Throwable $e) {
$messages[] = $e::class . ': ' . $e->getMessage();
}
}
return '';
}, 1);
$result = $statement->debugDumpParams();
ob_end_clean();

echo implode("\n", $messages), "\n";
var_dump($result);
var_dump($statement->execute());
var_dump($statement->fetchColumn());
?>
--CLEAN--
<?php
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, 'pdo_reentrant_debug_dump');
?>
--EXPECT--
Error: Cannot perform another operation on this PDOStatement while an operation is in progress
NULL
bool(true)
string(1) "1"
67 changes: 67 additions & 0 deletions ext/pdo/tests/pdo_stmt_reentrant_fetch_bound.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
--TEST--
PDO Common: PDOStatement rejects reentrant binding while updating a bound column
--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';
$db = PDOTest::factory();

final class ReenterOnDestruct
{
private mixed $replacement = null;
private WeakReference $statement;

public function __construct(PDOStatement $statement)
{
$this->statement = WeakReference::create($statement);
}

public function __destruct()
{
try {
$this->statement->get()->bindColumn(1, $this->replacement);
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}
}
}

$db->exec('CREATE TABLE pdo_reentrant_fetch_bound (id INT, val INT)');
$db->exec('INSERT INTO pdo_reentrant_fetch_bound VALUES (1, 1)');
$db->exec('INSERT INTO pdo_reentrant_fetch_bound VALUES (2, 2)');
$statement = $db->query('SELECT val FROM pdo_reentrant_fetch_bound ORDER BY id');

$value = new ReenterOnDestruct($statement);
$statement->bindColumn(1, $value, PDO::PARAM_INT);
var_dump($statement->fetch(PDO::FETCH_BOUND));
var_dump($value);

$statement->bindColumn(1, $value, PDO::PARAM_INT);
var_dump($statement->fetch(PDO::FETCH_BOUND));
var_dump($value);
?>
--CLEAN--
<?php
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, 'pdo_reentrant_fetch_bound');
?>
--EXPECT--
Error: Cannot perform another operation on this PDOStatement while an operation is in progress
bool(true)
string(1) "1"
bool(true)
string(1) "2"
64 changes: 64 additions & 0 deletions ext/pdo/tests/pdo_stmt_reentrant_fetch_mode.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
--TEST--
PDO Common: PDOStatement rejects reentrant binding while changing the default fetch 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';
$db = PDOTest::factory();

final class FetchTarget
{
private WeakReference $statement;

public function __construct(PDOStatement $statement)
{
$this->statement = WeakReference::create($statement);
}

public function __destruct()
{
try {
$this->statement->get()->bindValue(1, 2, PDO::PARAM_INT);
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}
}
}

$db->exec('CREATE TABLE pdo_reentrant_fetch_mode (val INT)');
$db->exec('INSERT INTO pdo_reentrant_fetch_mode VALUES (1)');
$statement = $db->prepare('SELECT val FROM pdo_reentrant_fetch_mode WHERE val = ?');
$target = new FetchTarget($statement);
$statement->setFetchMode(PDO::FETCH_INTO, $target);
unset($target);

var_dump($statement->setFetchMode(PDO::FETCH_ASSOC));
var_dump($statement->bindValue(1, 1, PDO::PARAM_INT));
var_dump($statement->execute());
var_dump($statement->fetchColumn());
?>
--CLEAN--
<?php
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, 'pdo_reentrant_fetch_mode');
?>
--EXPECT--
Error: Cannot perform another operation on this PDOStatement while an operation is in progress
bool(true)
bool(true)
bool(true)
string(1) "1"
Loading
Loading