Skip to content

Fix crash when re-executing a statement after closeCursor() - #46

Merged
iliaal merged 2 commits into
php:mainfrom
iliaal:fix-closecursor-reexecute
Oct 29, 2026
Merged

iliaal merged 2 commits into
php:mainfrom
iliaal:fix-closecursor-reexecute

Conversation

@iliaal

@iliaal iliaal commented Oct 16, 2026

Copy link
Copy Markdown
Member

closeCursor() clears stmt->executed, so the next execute() reallocates S->cols while PDO core keeps its column descriptions and never describes them again; the OCI defines keep writing into the freed buffers and the next fetch crashes. Dropping PDO's descriptions with php_pdo_stmt_set_column_count(stmt, 0), as pdo_sqlsrv does, makes core describe the columns into the new buffers. PHP 8.4.27RC1, 8.5.12RC1 and 8.6.0RC3 reach the same branch on every re-execute (#44, php/php-src#24372), so this covers that too; the core side is fixed separately.

closeCursor() clears stmt->executed, so the next execute() reallocates
S->cols, but PDO core keeps its column descriptions and skips describing
them again. The OCI defines then keep writing into the freed buffers and
the next fetch crashes. Drop PDO's column descriptions when reallocating
so they are described into the new buffers. The same path is reached on
any re-execute with PHP 8.4.27RC1, 8.5.12RC1 and 8.6.0RC3, see php#44.
@iliaal

iliaal commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

The 8.4/8.5/master failures are the php-src common PDO tests run from the branch head against the released setup-php binary (8.5.11 here); those tests assert fixes newer than that release, so main fails the same way. The new test passes on all lanes.

@m3ct0n

m3ct0n commented Oct 23, 2026

Copy link
Copy Markdown

Thanks for this. With this branch on PHP 8.4.27 the SELECT crash is gone for us (Debian 13, Instant Client 19.12, Oracle 11.2.0.4), but re-executing a non-query statement still fails.

pdo_oci has no cursor_closer, so on 8.4.27 / 8.5.12 PDO drains the previous result through the fetcher before every re-execute. That calls OCIStmtFetch2() on DML / DDL handles too, and the next OCIStmtExecute() fails:

$st = $dbh->prepare("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'");
$st->execute();
$st->execute(); // ORA-01002: fetch out of sequence

yiisoft/yii2#21122 reports ORA-03137 for a re-executed UPDATE, same cause.

Would you consider adding a cursor closer that cancels the cursor of a query only, with the same zero-row fetch oci_stmt_execute() already uses?

@@ -1020,6 +1020,20 @@ static int oci_stmt_col_meta(pdo_stmt_t *stmt, zend_long colno, zval *return_val
 	return SUCCESS;
 } /* }}} */
 
+static int oci_stmt_cursor_closer(pdo_stmt_t *stmt) /* {{{ */
+{
+	pdo_oci_stmt *S = (pdo_oci_stmt*)stmt->driver_data;
+
+	/* Without a closer, PDO drains the previous result with the fetcher, even on
+	 * DML / DDL handles (ORA-01002, ORA-03137 on PHP 8.4.27 / 8.5.12: GH-24372).
+	 * Only a query has a cursor: cancel it, as oci_stmt_execute() does. */
+	if (S && S->stmt && S->stmt_type == OCI_STMT_SELECT) {
+		OCIStmtFetch(S->stmt, S->err, 0, OCI_FETCH_NEXT, OCI_DEFAULT);
+	}
+
+	return 1;
+} /* }}} */
+
 const struct pdo_stmt_methods oci_stmt_methods = {
 	oci_stmt_dtor,
 	oci_stmt_execute,
@@ -1031,5 +1045,5 @@ const struct pdo_stmt_methods oci_stmt_methods = {
 	NULL, /* get_attr */
 	oci_stmt_col_meta,
 	NULL,
-	NULL
+	oci_stmt_cursor_closer
 };

With the core fix (d5696b8) the closer is also what PDO calls on re-execute, and closeCursor() now cancels the cursor in one call instead of fetching the remaining rows.

Tested on PHP 8.4.27 with this branch + the closer: SELECT re-executed with binds, partially fetched SELECT re-executed, SELECT with a BLOB column, closeCursor() + execute() (also with two closeCursor() calls), ALTER SESSION executed 3 times, PL/SQL block with IN/OUT binds. Without the closer, the ALTER SESSION case fails with ORA-01002.

I could not run DML on a real table here, so I added a test for CI. On 8.4.27 its ALTER SESSION part fails without the closer; on releases without the regression it passes either way:

tests/pdo_oci_reexecute_dml.phpt
--TEST--
PDO_OCI: Re-executing DML and DDL statements
--EXTENSIONS--
pdo
pdo_oci
--SKIPIF--
<?php
require(getenv('PDO_TEST_DIR').'/pdo_test.inc');
PDOTest::skip();
?>
--FILE--
<?php

require_once(getenv('PDO_TEST_DIR').'/pdo_test.inc');

$dbh = PDOTest::factory();
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$dbh->exec("begin
             execute immediate 'drop table test_reexecute_dml';
             exception when others then
               if sqlcode <> -942 then
                 raise;
               end if;
           end;");
$dbh->exec('CREATE TABLE test_reexecute_dml (id NUMBER NOT NULL PRIMARY KEY, name VARCHAR2(20))');

$insert = $dbh->prepare('INSERT INTO test_reexecute_dml (id, name) VALUES (:id, :name)');
foreach ([1 => 'a', 2 => 'b', 3 => 'c'] as $id => $name) {
    $insert->execute([':id' => $id, ':name' => $name]);
}

$update = $dbh->prepare('UPDATE test_reexecute_dml SET name = :name WHERE id = :id');
$update->execute([':id' => 1, ':name' => 'a1']);
$update->execute([':id' => 2, ':name' => 'b1']);
$update->execute([':id' => 2, ':name' => 'b2']);
var_dump($update->rowCount());

$alter = $dbh->prepare("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'");
$alter->execute();
$alter->execute();
$alter->execute();
echo "alter session: ok\n";

$select = $dbh->prepare('SELECT name FROM test_reexecute_dml WHERE id = :id');
foreach ([1, 2, 3] as $id) {
    $select->execute([':id' => $id]);
    echo $select->fetchColumn(), "\n";
}

?>
--CLEAN--
<?php
require_once(getenv('PDO_TEST_DIR').'/pdo_test.inc');
$dbh = PDOTest::factory();
$dbh->exec('DROP TABLE test_reexecute_dml');
?>
--EXPECT--
int(1)
alter session: ok
a1
b2
c

The commit (closer + test + package.xml entry) is on top of this branch here: https://github.com/m3ct0n/pecl-database-pdo_oci/tree/pdo-oci-cursor-closer. I can open it as a PR against your branch if that's easier.

matyhtf added a commit to swoole/swoole-src that referenced this pull request Oct 23, 2026
…acos-unit][win-unit]

Reset PDO column descriptions before replacing OCI define buffers, adapting php/pecl-database-pdo_oci#46. Provide an OCI cursor closer so PDO does not drain DML/RETURNING statements with a SELECT fetch before re-execution on PHP 8.4.27.

Cover exhausted and partially read cursor reuse in blocking and coroutine modes. Exercise explicit cursor closing between LOB executions and isolate fixture tables so crashes do not poison retries.

Validation: reproduced SIGSEGV and both ORA-03137 LOB failures before the fix; all four focused Oracle tests pass after the fix. The full directory has 33 passes, 4 version skips, and 3 pre-existing client error-format mismatches, verified identical with the original driver.
Register a cursor closer that cancels SELECT cursors with a zero-row fetch.
PDO's fallback fetches from DML, DDL, and PL/SQL handles, which can make
the next execute fail with ORA-01002 or ORA-03137. This affects explicit
closeCursor() and automatic cleanup in PHP 8.4.27 and 8.5.12.

Cover non-query re-execution with and without closeCursor(), and extend
query coverage to partial fetches, changed binds, repeated closure, and
scrollable cursors. Based on m3ct0n's suggestion and regression test.
@iliaal

iliaal commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Added the query-only closer and regression coverage for DML, ALTER SESSION, PL/SQL, and partially fetched queries. Verified on PHP 8.4.26 and 8.4.27, including explicit closeCursor(), repeated closure, and scrollable cursors. Thanks for the reproducer and proposed fix.

@sharadraju sharadraju left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@iliaal
iliaal merged commit d94b64b into php:main Oct 29, 2026
13 of 27 checks passed
@terabytesoftw

Copy link
Copy Markdown

Thks 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants