Commit d5696b8cd10 for php
commit d5696b8cd10b151ced5f93d4d365e4aad1c0c2c3
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Fri Oct 16 15:16:38 2026 -0400
Fix GH-24372: Keep stmt->executed set across PDOStatement::execute()
11690db1787 cleared stmt->executed on entry to execute() so a failed
execute() could not leave the previous result fetchable. Drivers read that
flag as "executed successfully at least once": pdo_oci reallocates its
column buffers when it is clear, while PDO core skips describing the
columns again because they are still set, so the OCI defines keep writing
into the freed buffers; pdo_ibm leaks its column descriptors. Record the
failed execute() in a separate bit instead. The cursor closer still runs
on entry, but drivers without one are no longer drained, which also made
them fetch every remaining row on each re-execute.
Fixes GH-24372
Closes GH-24379
diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c
index 52a75010bb2..1c06ac21eb0 100644
--- a/ext/pdo/pdo_stmt.c
+++ b/ext/pdo/pdo_stmt.c
@@ -391,29 +391,6 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_
}
/* }}} */
-static bool pdo_stmt_do_next_rowset(pdo_stmt_t *stmt);
-
-static void pdo_stmt_invalidate_result(pdo_stmt_t *stmt)
-{
- if (stmt->methods->cursor_closer) {
- stmt->methods->cursor_closer(stmt);
- } else {
- do {
- while (stmt->methods->fetcher(stmt, PDO_FETCH_ORI_NEXT, 0))
- ;
- if (!stmt->methods->next_rowset) {
- break;
- }
-
- if (!pdo_stmt_do_next_rowset(stmt)) {
- break;
- }
- } while (1);
- }
-
- stmt->executed = 0;
-}
-
/* {{{ Execute a prepared statement, optionally binding parameters */
PHP_METHOD(PDOStatement, execute)
{
@@ -430,9 +407,10 @@ PHP_METHOD(PDOStatement, execute)
RETURN_THROWS();
}
- if (stmt->executed) {
- pdo_stmt_invalidate_result(stmt);
+ if (stmt->executed && stmt->methods->cursor_closer) {
+ stmt->methods->cursor_closer(stmt);
}
+ stmt->execute_incomplete = 1;
PDO_STMT_CLEAR_ERR();
if (input_params) {
@@ -499,6 +477,7 @@ PHP_METHOD(PDOStatement, execute)
RETURN_FALSE;
}
if (stmt->methods->executer(stmt)) {
+ stmt->execute_incomplete = 0;
if (!stmt->executed) {
/* this is the first execute */
@@ -605,7 +584,7 @@ static inline void fetch_value(pdo_stmt_t *stmt, zval *dest, int colno, enum pdo
static bool do_fetch_common(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori, zend_long offset) /* {{{ */
{
- if (!stmt->executed) {
+ if (!stmt->executed || stmt->execute_incomplete) {
return 0;
}
diff --git a/ext/pdo/php_pdo_driver.h b/ext/pdo/php_pdo_driver.h
index 3f9ef4e214d..0d39bf9f463 100644
--- a/ext/pdo/php_pdo_driver.h
+++ b/ext/pdo/php_pdo_driver.h
@@ -568,8 +568,9 @@ struct _pdo_stmt_t {
* emulate prepare and bind on its behalf */
unsigned supports_placeholders:2;
unsigned in_param_event:1;
+ unsigned execute_incomplete:1;
- unsigned _reserved:28;
+ unsigned _reserved:27;
/* the number of columns in the result set; not valid until after
* the statement has been executed at least once. In some cases, might