From 4e7540255f6684db473dd50836705f1e58b3120e Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Tue, 14 Jul 2026 22:24:48 -0400 Subject: [PATCH] Prevent reentrant PDOStatement operations Reject same-statement reentry while PDO traverses or mutates live binding and fetch state. Keep the guard across Fiber suspension and invalidate weak references before callback-capable statement teardown. --- ext/pdo/pdo_stmt.c | 215 ++++++++++++++++-- ext/pdo/php_pdo_driver.h | 3 +- ..._class_change_ctor_args_during_fetch5.phpt | 3 +- ext/pdo/tests/pdo_stmt_reentrant_bind.phpt | 124 ++++++++++ .../tests/pdo_stmt_reentrant_debug_dump.phpt | 59 +++++ .../tests/pdo_stmt_reentrant_fetch_bound.phpt | 67 ++++++ .../tests/pdo_stmt_reentrant_fetch_mode.phpt | 64 ++++++ ext/pdo/tests/pdo_stmt_reentrant_fiber.phpt | 72 ++++++ .../pdo_stmt_weakref_during_destruction.phpt | 49 ++++ 9 files changed, 628 insertions(+), 28 deletions(-) create mode 100644 ext/pdo/tests/pdo_stmt_reentrant_bind.phpt create mode 100644 ext/pdo/tests/pdo_stmt_reentrant_debug_dump.phpt create mode 100644 ext/pdo/tests/pdo_stmt_reentrant_fetch_bound.phpt create mode 100644 ext/pdo/tests/pdo_stmt_reentrant_fetch_mode.phpt create mode 100644 ext/pdo/tests/pdo_stmt_reentrant_fiber.phpt create mode 100644 ext/pdo/tests/pdo_stmt_weakref_during_destruction.phpt diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c index 09b9fd3550e6..7f62480f2628 100644 --- a/ext/pdo/pdo_stmt.c +++ b/ext/pdo/pdo_stmt.c @@ -29,6 +29,7 @@ #include "php_pdo_int.h" #include "zend_exceptions.h" #include "zend_interfaces.h" +#include "zend_weakrefs.h" #include "php_memory_streams.h" #include "pdo_stmt_arginfo.h" @@ -39,6 +40,42 @@ RETURN_THROWS(); \ } \ +static bool pdo_stmt_enter_operation(pdo_stmt_t *stmt) +{ + if (UNEXPECTED(stmt->in_operation)) { + zend_throw_error(NULL, "Cannot perform another operation on this PDOStatement while an operation is in progress"); + return false; + } + + stmt->in_operation = true; + return true; +} + +static void pdo_stmt_leave_operation(pdo_stmt_t *stmt) +{ + stmt->in_fetch = false; + stmt->in_operation = false; +} + +static void pdo_stmt_call_operation(pdo_stmt_t *stmt, zif_handler operation, INTERNAL_FUNCTION_PARAMETERS) +{ + if (!pdo_stmt_enter_operation(stmt)) { + return; + } + + bool bailout = false; + zend_try { + operation(INTERNAL_FUNCTION_PARAM_PASSTHRU); + } zend_catch { + bailout = true; + } zend_end_try(); + + pdo_stmt_leave_operation(stmt); + if (bailout) { + zend_bailout(); + } +} + static inline bool rewrite_name_to_position(pdo_stmt_t *stmt, struct pdo_bound_param_data *param) /* {{{ */ { if (stmt->bound_param_map) { @@ -377,7 +414,7 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_ /* }}} */ /* {{{ Execute a prepared statement, optionally binding parameters */ -PHP_METHOD(PDOStatement, execute) +static ZEND_NAMED_FUNCTION(pdo_stmt_execute) { zval *input_params = NULL; int ret = 1; @@ -478,6 +515,12 @@ PHP_METHOD(PDOStatement, execute) } /* }}} */ +PHP_METHOD(PDOStatement, execute) +{ + PHP_STMT_GET_OBJ; + pdo_stmt_call_operation(stmt, pdo_stmt_execute, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + static inline void fetch_value(pdo_stmt_t *stmt, zval *dest, int colno, enum pdo_param_type *type_override) /* {{{ */ { if (colno < 0) { @@ -611,11 +654,14 @@ static bool do_fetch_common(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori, ze static bool pdo_do_key_pair_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori, zend_long offset, HashTable *container) { + stmt->in_fetch = true; if (!do_fetch_common(stmt, ori, offset)) { + stmt->in_fetch = false; return false; } if (stmt->column_count != 2) { pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "PDO::FETCH_KEY_PAIR fetch mode requires the result set to contain exactly 2 columns."); + stmt->in_fetch = false; return false; } @@ -630,6 +676,7 @@ static bool pdo_do_key_pair_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation o zend_symtable_update(container, Z_STR(key), &val); } zval_ptr_dtor(&key); + stmt->in_fetch = false; return true; } @@ -667,6 +714,7 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h int column_index_to_fetch = 0; zval *fetch_function_params = NULL; uint32_t fetch_function_param_num = 0; + stmt->in_fetch = true; if (how == PDO_FETCH_USE_DEFAULT) { how = stmt->default_fetch_type; @@ -675,16 +723,18 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h how = how & ~PDO_FETCH_FLAGS; if (!do_fetch_common(stmt, ori, offset)) { - return false; + goto in_fetch_error; } if (how == PDO_FETCH_BOUND) { RETVAL_TRUE; + stmt->in_fetch = false; return true; } if (how == PDO_FETCH_LAZY) { pdo_get_lazy_object(stmt, return_value); + stmt->in_fetch = false; return true; } @@ -698,12 +748,12 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h if (colno < 0 ) { zend_value_error("Column index must be greater than or equal to 0"); - return false; + goto in_fetch_error; } if (colno >= stmt->column_count) { zend_value_error("Invalid column index"); - return false; + goto in_fetch_error; } if (flags == PDO_FETCH_GROUP && stmt->fetch.column == -1) { @@ -722,10 +772,10 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h } convert_to_string(group_key); } + stmt->in_fetch = false; return true; } - stmt->in_fetch = true; switch (how) { case PDO_FETCH_USE_DEFAULT: case PDO_FETCH_ASSOC: @@ -944,6 +994,28 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h } /* }}} */ +static bool pdo_stmt_do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type how, + enum pdo_fetch_orientation ori, zend_long offset, zval *group_key) +{ + if (!pdo_stmt_enter_operation(stmt)) { + return false; + } + + bool bailout = false; + bool result = false; + zend_try { + result = do_fetch(stmt, return_value, how, ori, offset, group_key); + } zend_catch { + bailout = true; + } zend_end_try(); + + pdo_stmt_leave_operation(stmt); + if (bailout) { + zend_bailout(); + } + return result; +} + // TODO Error on the following cases: // Combining PDO_FETCH_UNIQUE and PDO_FETCH_GROUP @@ -1026,7 +1098,7 @@ static bool pdo_verify_fetch_mode(uint32_t default_mode_and_flags, zend_long mod /* }}} */ /* {{{ Fetches the next row and returns it, or false if there are no more rows */ -PHP_METHOD(PDOStatement, fetch) +static ZEND_NAMED_FUNCTION(pdo_stmt_fetch) { zend_long how = PDO_FETCH_USE_DEFAULT; zend_long ori = PDO_FETCH_ORI_NEXT; @@ -1064,8 +1136,14 @@ PHP_METHOD(PDOStatement, fetch) } /* }}} */ +PHP_METHOD(PDOStatement, fetch) +{ + PHP_STMT_GET_OBJ; + pdo_stmt_call_operation(stmt, pdo_stmt_fetch, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + /* {{{ Fetches the next row and returns it as an object. */ -PHP_METHOD(PDOStatement, fetchObject) +static ZEND_NAMED_FUNCTION(pdo_stmt_fetch_object) { zend_class_entry *ce = NULL; zend_class_entry *old_ce; @@ -1104,8 +1182,14 @@ PHP_METHOD(PDOStatement, fetchObject) } /* }}} */ +PHP_METHOD(PDOStatement, fetchObject) +{ + PHP_STMT_GET_OBJ; + pdo_stmt_call_operation(stmt, pdo_stmt_fetch_object, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + /* {{{ Returns a data of the specified column in the result set. */ -PHP_METHOD(PDOStatement, fetchColumn) +static ZEND_NAMED_FUNCTION(pdo_stmt_fetch_column) { zend_long col_n = 0; @@ -1117,15 +1201,24 @@ PHP_METHOD(PDOStatement, fetchColumn) PHP_STMT_GET_OBJ; PDO_STMT_CLEAR_ERR(); + stmt->in_fetch = true; if (!do_fetch_common(stmt, PDO_FETCH_ORI_NEXT, 0)) { + stmt->in_fetch = false; PDO_HANDLE_STMT_ERR(); RETURN_FALSE; } fetch_value(stmt, return_value, col_n, NULL); + stmt->in_fetch = false; } /* }}} */ +PHP_METHOD(PDOStatement, fetchColumn) +{ + PHP_STMT_GET_OBJ; + pdo_stmt_call_operation(stmt, pdo_stmt_fetch_column, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + static bool pdo_get_fcc_from_zval(zend_fcall_info_cache *fcc, zval *callable) { if (callable == NULL) { /* TODO use "must be of type callable" format? */ @@ -1147,7 +1240,7 @@ static bool pdo_get_fcc_from_zval(zend_fcall_info_cache *fcc, zval *callable) { } /* {{{ Returns an array of all of the results. */ -PHP_METHOD(PDOStatement, fetchAll) +static ZEND_NAMED_FUNCTION(pdo_stmt_fetch_all) { zend_long how = PDO_FETCH_USE_DEFAULT; zval *arg2 = NULL; @@ -1304,6 +1397,12 @@ PHP_METHOD(PDOStatement, fetchAll) } /* }}} */ +PHP_METHOD(PDOStatement, fetchAll) +{ + PHP_STMT_GET_OBJ; + pdo_stmt_call_operation(stmt, pdo_stmt_fetch_all, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + static void register_bound_param(INTERNAL_FUNCTION_PARAMETERS, int is_param) /* {{{ */ { struct pdo_bound_param_data param; @@ -1355,7 +1454,7 @@ static void register_bound_param(INTERNAL_FUNCTION_PARAMETERS, int is_param) /* } /* }}} */ /* {{{ bind an input parameter to the value of a PHP variable. $paramno is the 1-based position of the placeholder in the SQL statement (but can be the parameter name for drivers that support named placeholders). It should be called prior to execute(). */ -PHP_METHOD(PDOStatement, bindValue) +static ZEND_NAMED_FUNCTION(pdo_stmt_bind_value) { struct pdo_bound_param_data param; zend_long param_type = PDO_PARAM_STR; @@ -1398,20 +1497,38 @@ PHP_METHOD(PDOStatement, bindValue) } /* }}} */ +PHP_METHOD(PDOStatement, bindValue) +{ + PHP_STMT_GET_OBJ; + pdo_stmt_call_operation(stmt, pdo_stmt_bind_value, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + /* {{{ bind a parameter to a PHP variable. $paramno is the 1-based position of the placeholder in the SQL statement (but can be the parameter name for drivers that support named placeholders). This isn't supported by all drivers. It should be called prior to execute(). */ -PHP_METHOD(PDOStatement, bindParam) +static ZEND_NAMED_FUNCTION(pdo_stmt_bind_param) { register_bound_param(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1); } /* }}} */ +PHP_METHOD(PDOStatement, bindParam) +{ + PHP_STMT_GET_OBJ; + pdo_stmt_call_operation(stmt, pdo_stmt_bind_param, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + /* {{{ bind a column to a PHP variable. On each row fetch $param will contain the value of the corresponding column. $column is the 1-based offset of the column, or the column name. For portability, don't call this before execute(). */ -PHP_METHOD(PDOStatement, bindColumn) +static ZEND_NAMED_FUNCTION(pdo_stmt_bind_column) { register_bound_param(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0); } /* }}} */ +PHP_METHOD(PDOStatement, bindColumn) +{ + PHP_STMT_GET_OBJ; + pdo_stmt_call_operation(stmt, pdo_stmt_bind_column, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + /* {{{ Returns the number of rows in a result set, or the number of rows affected by the last execute(). It is not always meaningful. */ PHP_METHOD(PDOStatement, rowCount) { @@ -1469,7 +1586,7 @@ PHP_METHOD(PDOStatement, errorInfo) /* }}} */ /* {{{ Set an attribute */ -PHP_METHOD(PDOStatement, setAttribute) +static ZEND_NAMED_FUNCTION(pdo_stmt_set_attribute) { zend_long attr; zval *value = NULL; @@ -1498,6 +1615,12 @@ PHP_METHOD(PDOStatement, setAttribute) } /* }}} */ +PHP_METHOD(PDOStatement, setAttribute) +{ + PHP_STMT_GET_OBJ; + pdo_stmt_call_operation(stmt, pdo_stmt_set_attribute, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + /* {{{ Get an attribute */ static bool generic_stmt_attr_get(pdo_stmt_t *stmt, zval *return_value, zend_long attr) @@ -1510,7 +1633,7 @@ static bool generic_stmt_attr_get(pdo_stmt_t *stmt, zval *return_value, zend_lon return false; } -PHP_METHOD(PDOStatement, getAttribute) +static ZEND_NAMED_FUNCTION(pdo_stmt_get_attribute) { zend_long attr; @@ -1549,6 +1672,12 @@ PHP_METHOD(PDOStatement, getAttribute) } /* }}} */ +PHP_METHOD(PDOStatement, getAttribute) +{ + PHP_STMT_GET_OBJ; + pdo_stmt_call_operation(stmt, pdo_stmt_get_attribute, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + /* {{{ Returns the number of columns in the result set */ PHP_METHOD(PDOStatement, columnCount) { @@ -1560,7 +1689,7 @@ PHP_METHOD(PDOStatement, columnCount) /* }}} */ /* {{{ Returns meta data for a numbered column */ -PHP_METHOD(PDOStatement, getColumnMeta) +static ZEND_NAMED_FUNCTION(pdo_stmt_get_column_meta) { zend_long colno; struct pdo_column_data *col; @@ -1594,6 +1723,12 @@ PHP_METHOD(PDOStatement, getColumnMeta) } /* }}} */ +PHP_METHOD(PDOStatement, getColumnMeta) +{ + PHP_STMT_GET_OBJ; + pdo_stmt_call_operation(stmt, pdo_stmt_get_column_meta, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + void pdo_stmt_free_default_fetch_mode(pdo_stmt_t *stmt) { enum pdo_fetch_type default_fetch_mode = stmt->default_fetch_type & ~PDO_FETCH_FLAGS; @@ -1752,7 +1887,7 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a return true; } -PHP_METHOD(PDOStatement, setFetchMode) +static ZEND_NAMED_FUNCTION(pdo_stmt_set_fetch_mode) { zend_long fetch_mode; zval *args = NULL; @@ -1764,10 +1899,6 @@ PHP_METHOD(PDOStatement, setFetchMode) PHP_STMT_GET_OBJ; - if (stmt->in_fetch) { - zend_throw_error(NULL, "Cannot change default fetch mode while fetching"); - RETURN_THROWS(); - } if (!pdo_stmt_setup_fetch_mode(stmt, fetch_mode, 1, args, num_args)) { RETURN_THROWS(); } @@ -1777,6 +1908,16 @@ PHP_METHOD(PDOStatement, setFetchMode) } /* }}} */ +PHP_METHOD(PDOStatement, setFetchMode) +{ + PHP_STMT_GET_OBJ; + if (stmt->in_fetch) { + zend_throw_error(NULL, "Cannot change default fetch mode while fetching"); + RETURN_THROWS(); + } + pdo_stmt_call_operation(stmt, pdo_stmt_set_fetch_mode, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + /* {{{ Advances to the next rowset in a multi-rowset statement handle. Returns true if it succeeded, false otherwise */ static bool pdo_stmt_do_next_rowset(pdo_stmt_t *stmt) @@ -1794,7 +1935,7 @@ static bool pdo_stmt_do_next_rowset(pdo_stmt_t *stmt) return true; } -PHP_METHOD(PDOStatement, nextRowset) +static ZEND_NAMED_FUNCTION(pdo_stmt_next_rowset) { ZEND_PARSE_PARAMETERS_NONE(); @@ -1815,8 +1956,14 @@ PHP_METHOD(PDOStatement, nextRowset) } /* }}} */ +PHP_METHOD(PDOStatement, nextRowset) +{ + PHP_STMT_GET_OBJ; + pdo_stmt_call_operation(stmt, pdo_stmt_next_rowset, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + /* {{{ Closes the cursor, leaving the statement ready for re-execution. */ -PHP_METHOD(PDOStatement, closeCursor) +static ZEND_NAMED_FUNCTION(pdo_stmt_close_cursor) { ZEND_PARSE_PARAMETERS_NONE(); @@ -1850,8 +1997,14 @@ PHP_METHOD(PDOStatement, closeCursor) } /* }}} */ +PHP_METHOD(PDOStatement, closeCursor) +{ + PHP_STMT_GET_OBJ; + pdo_stmt_call_operation(stmt, pdo_stmt_close_cursor, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + /* {{{ A utility for internals hackers to debug parameter internals */ -PHP_METHOD(PDOStatement, debugDumpParams) +static ZEND_NAMED_FUNCTION(pdo_stmt_debug_dump_params) { ZEND_PARSE_PARAMETERS_NONE(); @@ -1911,6 +2064,12 @@ PHP_METHOD(PDOStatement, debugDumpParams) } /* }}} */ +PHP_METHOD(PDOStatement, debugDumpParams) +{ + PHP_STMT_GET_OBJ; + pdo_stmt_call_operation(stmt, pdo_stmt_debug_dump_params, INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + PHP_METHOD(PDOStatement, getIterator) { ZEND_PARSE_PARAMETERS_NONE(); @@ -2012,6 +2171,12 @@ zend_object_handlers pdo_row_object_handlers; PDO_API void php_pdo_free_statement(pdo_stmt_t *stmt) { + stmt->in_operation = true; + if (UNEXPECTED(GC_FLAGS(&stmt->std) & IS_OBJ_WEAKLY_REFERENCED)) { + zend_weakrefs_notify(&stmt->std); + GC_DEL_FLAGS(&stmt->std, IS_OBJ_WEAKLY_REFERENCED); + } + if (stmt->bound_params) { zend_hash_destroy(stmt->bound_params); FREE_HASHTABLE(stmt->bound_params); @@ -2126,7 +2291,7 @@ static void pdo_stmt_iter_move_forwards(zend_object_iterator *iter) ZVAL_UNDEF(&I->fetch_ahead); } - if (!do_fetch(stmt, &I->fetch_ahead, PDO_FETCH_USE_DEFAULT, + if (!pdo_stmt_do_fetch(stmt, &I->fetch_ahead, PDO_FETCH_USE_DEFAULT, PDO_FETCH_ORI_NEXT, /* offset */ 0, NULL)) { PDO_HANDLE_STMT_ERR(); I->key = (zend_ulong)-1; @@ -2168,7 +2333,7 @@ zend_object_iterator *pdo_stmt_iter_get(zend_class_entry *ce, zval *object, int Z_ADDREF_P(object); ZVAL_OBJ(&I->iter.data, Z_OBJ_P(object)); - if (!do_fetch(stmt, &I->fetch_ahead, PDO_FETCH_USE_DEFAULT, + if (!pdo_stmt_do_fetch(stmt, &I->fetch_ahead, PDO_FETCH_USE_DEFAULT, PDO_FETCH_ORI_NEXT, /* offset */ 0, NULL)) { PDO_HANDLE_STMT_ERR(); I->key = (zend_ulong)-1; diff --git a/ext/pdo/php_pdo_driver.h b/ext/pdo/php_pdo_driver.h index 479a1b5436c9..639d2e7ef87d 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_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 */ diff --git a/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch5.phpt b/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch5.phpt index c4b854023ffc..00c0c7b4d036 100644 --- a/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch5.phpt +++ b/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch5.phpt @@ -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 diff --git a/ext/pdo/tests/pdo_stmt_reentrant_bind.phpt b/ext/pdo/tests/pdo_stmt_reentrant_bind.phpt new file mode 100644 index 000000000000..d91567228e53 --- /dev/null +++ b/ext/pdo/tests/pdo_stmt_reentrant_bind.phpt @@ -0,0 +1,124 @@ +--TEST-- +PDO Common: PDOStatement rejects reentrant operations while changing parameter bindings +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +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-- + +--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" diff --git a/ext/pdo/tests/pdo_stmt_reentrant_debug_dump.phpt b/ext/pdo/tests/pdo_stmt_reentrant_debug_dump.phpt new file mode 100644 index 000000000000..049aeadab213 --- /dev/null +++ b/ext/pdo/tests/pdo_stmt_reentrant_debug_dump.phpt @@ -0,0 +1,59 @@ +--TEST-- +PDO Common: PDOStatement rejects reentrant binding from a debug output handler +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +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-- + +--EXPECT-- +Error: Cannot perform another operation on this PDOStatement while an operation is in progress +NULL +bool(true) +string(1) "1" diff --git a/ext/pdo/tests/pdo_stmt_reentrant_fetch_bound.phpt b/ext/pdo/tests/pdo_stmt_reentrant_fetch_bound.phpt new file mode 100644 index 000000000000..617de02aa868 --- /dev/null +++ b/ext/pdo/tests/pdo_stmt_reentrant_fetch_bound.phpt @@ -0,0 +1,67 @@ +--TEST-- +PDO Common: PDOStatement rejects reentrant binding while updating a bound column +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +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-- + +--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" diff --git a/ext/pdo/tests/pdo_stmt_reentrant_fetch_mode.phpt b/ext/pdo/tests/pdo_stmt_reentrant_fetch_mode.phpt new file mode 100644 index 000000000000..bdb0e78626be --- /dev/null +++ b/ext/pdo/tests/pdo_stmt_reentrant_fetch_mode.phpt @@ -0,0 +1,64 @@ +--TEST-- +PDO Common: PDOStatement rejects reentrant binding while changing the default fetch mode +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +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-- + +--EXPECT-- +Error: Cannot perform another operation on this PDOStatement while an operation is in progress +bool(true) +bool(true) +bool(true) +string(1) "1" diff --git a/ext/pdo/tests/pdo_stmt_reentrant_fiber.phpt b/ext/pdo/tests/pdo_stmt_reentrant_fiber.phpt new file mode 100644 index 000000000000..e14b8921f337 --- /dev/null +++ b/ext/pdo/tests/pdo_stmt_reentrant_fiber.phpt @@ -0,0 +1,72 @@ +--TEST-- +PDO Common: PDOStatement operation guard remains held across Fiber suspension +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +exec('CREATE TABLE pdo_reentrant_fiber (val INT)'); +$db->exec('INSERT INTO pdo_reentrant_fiber VALUES (1)'); +$statement = $db->prepare('SELECT val FROM pdo_reentrant_fiber WHERE val = ?'); +$statement->execute([1]); + +set_error_handler(static function (): bool { + static $suspended = false; + if (!$suspended) { + $suspended = true; + Fiber::suspend('handler'); + } + return true; +}); + +$fetchFiber = new Fiber(static function () use ($statement): void { + $statement->fetch(PDO::FETCH_CLASS | PDO::FETCH_SERIALIZE); +}); +var_dump($fetchFiber->start()); + +$bindFiber = new Fiber(static function () use ($statement): void { + try { + $statement->bindValue(1, new SuspendOnString(), PDO::PARAM_STR); + } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + } +}); +$bindFiber->start(); + +var_dump($fetchFiber->resume()); +var_dump($fetchFiber->isTerminated()); +?> +--CLEAN-- + +--EXPECT-- +string(7) "handler" +Error: Cannot perform another operation on this PDOStatement while an operation is in progress +NULL +bool(true) diff --git a/ext/pdo/tests/pdo_stmt_weakref_during_destruction.phpt b/ext/pdo/tests/pdo_stmt_weakref_during_destruction.phpt new file mode 100644 index 000000000000..503397449c11 --- /dev/null +++ b/ext/pdo/tests/pdo_stmt_weakref_during_destruction.phpt @@ -0,0 +1,49 @@ +--TEST-- +PDO Common: PDOStatement invalidates weak references before statement destruction +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +exec('CREATE TABLE pdo_stmt_weakref_dtor (val INT)'); + +final class StoreStatementWeakReference +{ + public WeakReference $statement; + + public function __destruct() + { + $GLOBALS['statement'] = $this->statement->get(); + } +} + +$stmt = $db->prepare('SELECT val FROM pdo_stmt_weakref_dtor WHERE val = ?'); +$value = new StoreStatementWeakReference(); +$value->statement = WeakReference::create($stmt); +$stmt->bindValue(1, $value, PDO::PARAM_NULL); + +unset($value, $stmt); + +var_dump($statement); +?> +--CLEAN-- + +--EXPECT-- +NULL