From c68042e94332ccade26bdf5d00fd3f533898c70c Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 16 Oct 2026 15:16:38 -0400 Subject: [PATCH] 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 --- ext/pdo/pdo_stmt.c | 31 +++++-------------------------- ext/pdo/php_pdo_driver.h | 3 ++- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c index 52a75010bb23..1c06ac21eb0d 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 3f9ef4e214de..0d39bf9f4636 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