From ac4c145450692a2fff422a8fabb7ec03b83d1dd8 Mon Sep 17 00:00:00 2001 From: lazerg Date: Sat, 19 Sep 2026 01:59:20 +0500 Subject: [PATCH] Fix GH-23756: crash when a collation callback closes the statement cursor --- NEWS | 4 +++ ext/pdo_sqlite/php_pdo_sqlite_int.h | 1 + ext/pdo_sqlite/sqlite_statement.c | 44 +++++++++++++++++++++++++++-- ext/pdo_sqlite/tests/gh23756.phpt | 35 +++++++++++++++++++++++ ext/pdo_sqlite/tests/gh23756_2.phpt | 34 ++++++++++++++++++++++ ext/pdo_sqlite/tests/gh23756_3.phpt | 40 ++++++++++++++++++++++++++ ext/pdo_sqlite/tests/gh23756_4.phpt | 24 ++++++++++++++++ 7 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 ext/pdo_sqlite/tests/gh23756.phpt create mode 100644 ext/pdo_sqlite/tests/gh23756_2.phpt create mode 100644 ext/pdo_sqlite/tests/gh23756_3.phpt create mode 100644 ext/pdo_sqlite/tests/gh23756_4.phpt diff --git a/NEWS b/NEWS index c38d108d840a..8ce8e10528de 100644 --- a/NEWS +++ b/NEWS @@ -32,6 +32,10 @@ PHP NEWS . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid column index. (Ilia Alshanetsky) +- PDO Sqlite: + . Fixed bug GH-23756 (crash when a callback closes the cursor of or + re-executes the statement being executed). (Lazizbek Ergashev) + - Sockets: . Fixed socket_select() silently truncating sets larger than FD_SETSIZE on Windows. (David Carlier) diff --git a/ext/pdo_sqlite/php_pdo_sqlite_int.h b/ext/pdo_sqlite/php_pdo_sqlite_int.h index 43a07345ed6b..8ac0e4fdec35 100644 --- a/ext/pdo_sqlite/php_pdo_sqlite_int.h +++ b/ext/pdo_sqlite/php_pdo_sqlite_int.h @@ -57,6 +57,7 @@ typedef struct { sqlite3_stmt *stmt; unsigned pre_fetched:1; unsigned done:1; + unsigned stepping:1; } pdo_sqlite_stmt; extern const pdo_driver_t pdo_sqlite_driver; diff --git a/ext/pdo_sqlite/sqlite_statement.c b/ext/pdo_sqlite/sqlite_statement.c index c0e327450232..08e8984e8ef1 100644 --- a/ext/pdo_sqlite/sqlite_statement.c +++ b/ext/pdo_sqlite/sqlite_statement.c @@ -39,16 +39,41 @@ static int pdo_sqlite_stmt_dtor(pdo_stmt_t *stmt) return 1; } +static int pdo_sqlite_stmt_step(pdo_sqlite_stmt *S) +{ + int return_code; + bool bailout = false; + + S->stepping = 1; + zend_try { + return_code = sqlite3_step(S->stmt); + } zend_catch { + bailout = true; + } zend_end_try(); + S->stepping = 0; + + if (bailout) { + zend_bailout(); + } + + return return_code; +} + static int pdo_sqlite_stmt_execute(pdo_stmt_t *stmt) { pdo_sqlite_stmt *S = (pdo_sqlite_stmt*)stmt->driver_data; + if (S->stepping) { + zend_throw_error(NULL, "Cannot execute a PDOStatement while it is executing"); + return 0; + } + if (stmt->executed && !S->done) { sqlite3_reset(S->stmt); } S->done = 0; - switch (sqlite3_step(S->stmt)) { + switch (pdo_sqlite_stmt_step(S)) { case SQLITE_ROW: S->pre_fetched = 1; php_pdo_stmt_set_column_count(stmt, sqlite3_data_count(S->stmt)); @@ -80,6 +105,11 @@ static int pdo_sqlite_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_d switch (event_type) { case PDO_PARAM_EVT_EXEC_PRE: + if (S->stepping) { + zend_throw_error(NULL, "Cannot execute a PDOStatement while it is executing"); + return 0; + } + if (stmt->executed && !S->done) { sqlite3_reset(S->stmt); S->done = 1; @@ -214,7 +244,11 @@ static int pdo_sqlite_stmt_fetch(pdo_stmt_t *stmt, if (S->done) { return 0; } - i = sqlite3_step(S->stmt); + if (S->stepping) { + zend_throw_error(NULL, "Cannot fetch from a PDOStatement while it is executing"); + return 0; + } + i = pdo_sqlite_stmt_step(S); switch (i) { case SQLITE_ROW: return 1; @@ -363,6 +397,12 @@ static int pdo_sqlite_stmt_col_meta(pdo_stmt_t *stmt, zend_long colno, zval *ret static int pdo_sqlite_stmt_cursor_closer(pdo_stmt_t *stmt) { pdo_sqlite_stmt *S = (pdo_sqlite_stmt*)stmt->driver_data; + + if (S->stepping) { + zend_throw_error(NULL, "Cannot close the cursor of a PDOStatement while it is executing"); + return 0; + } + sqlite3_reset(S->stmt); return 1; } diff --git a/ext/pdo_sqlite/tests/gh23756.phpt b/ext/pdo_sqlite/tests/gh23756.phpt new file mode 100644 index 000000000000..27851795a80d --- /dev/null +++ b/ext/pdo_sqlite/tests/gh23756.phpt @@ -0,0 +1,35 @@ +--TEST-- +GH-23756 (crash when closeCursor() is called from a collation callback) +--EXTENSIONS-- +pdo_sqlite +--FILE-- +exec('CREATE TABLE t (x TEXT)'); +$db->exec("INSERT INTO t VALUES ('b'), ('a'), ('c')"); + +$stmt = null; +$db->createCollation('evil', function ($a, $b) use (&$stmt) { + $stmt->closeCursor(); + return $a <=> $b; +}); + +$stmt = $db->prepare('SELECT x FROM t ORDER BY x COLLATE evil'); +try { + $stmt->execute(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +var_dump($db->query('SELECT x FROM t ORDER BY x')->fetchAll(PDO::FETCH_COLUMN)); +?> +--EXPECT-- +Cannot close the cursor of a PDOStatement while it is executing +array(3) { + [0]=> + string(1) "a" + [1]=> + string(1) "b" + [2]=> + string(1) "c" +} diff --git a/ext/pdo_sqlite/tests/gh23756_2.phpt b/ext/pdo_sqlite/tests/gh23756_2.phpt new file mode 100644 index 000000000000..c48259df2ee7 --- /dev/null +++ b/ext/pdo_sqlite/tests/gh23756_2.phpt @@ -0,0 +1,34 @@ +--TEST-- +GH-23756 (crash when closeCursor() is called from a collation callback while fetching) +--EXTENSIONS-- +pdo_sqlite +--FILE-- +exec('CREATE TABLE t (x TEXT)'); +$db->exec("INSERT INTO t VALUES ('b'), ('a'), ('c')"); + +$stmt = null; +$armed = false; +$db->createCollation('evil', function ($a, $b) use (&$stmt, &$armed) { + if ($armed) { + $armed = false; + $stmt->closeCursor(); + } + return $a <=> $b; +}); + +$stmt = $db->prepare("SELECT x FROM t WHERE x <> 'zzz' COLLATE evil"); +$stmt->execute(); +var_dump($stmt->fetchColumn()); + +$armed = true; +try { + $stmt->fetchColumn(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +string(1) "b" +Cannot close the cursor of a PDOStatement while it is executing diff --git a/ext/pdo_sqlite/tests/gh23756_3.phpt b/ext/pdo_sqlite/tests/gh23756_3.phpt new file mode 100644 index 000000000000..7492ff26e755 --- /dev/null +++ b/ext/pdo_sqlite/tests/gh23756_3.phpt @@ -0,0 +1,40 @@ +--TEST-- +GH-23756 (crash when execute() is called from a collation callback) +--EXTENSIONS-- +pdo_sqlite +--FILE-- +exec('CREATE TABLE t (x TEXT)'); +$db->exec("INSERT INTO t VALUES ('b'), ('a'), ('c')"); + +$stmt = null; +$armed = false; +$db->createCollation('evil', function ($a, $b) use (&$stmt, &$armed) { + if ($armed) { + $armed = false; + $stmt->execute(); + } + return $a <=> $b; +}); + +$stmt = $db->prepare('SELECT x FROM t ORDER BY x COLLATE evil'); +$armed = true; +try { + $stmt->execute(); +} catch (Error $e) { + echo 'without parameters: ', $e->getMessage(), "\n"; +} + +$stmt = $db->prepare('SELECT x FROM t WHERE x <> :p ORDER BY x COLLATE evil'); +$stmt->bindValue(':p', 'zzz'); +$armed = true; +try { + $stmt->execute(); +} catch (Error $e) { + echo 'with a bound parameter: ', $e->getMessage(), "\n"; +} +?> +--EXPECT-- +without parameters: Cannot execute a PDOStatement while it is executing +with a bound parameter: Cannot execute a PDOStatement while it is executing diff --git a/ext/pdo_sqlite/tests/gh23756_4.phpt b/ext/pdo_sqlite/tests/gh23756_4.phpt new file mode 100644 index 000000000000..3f7404988335 --- /dev/null +++ b/ext/pdo_sqlite/tests/gh23756_4.phpt @@ -0,0 +1,24 @@ +--TEST-- +GH-23756 (closeCursor() still works after a collation callback bails out) +--EXTENSIONS-- +pdo_sqlite +--FILE-- +exec('CREATE TABLE t (x TEXT)'); +$db->exec("INSERT INTO t VALUES ('b'), ('a'), ('c')"); + +$db->createCollation('evil', function ($a, $b) { + exit("bailing out\n"); +}); + +register_shutdown_function(function () use (&$stmt) { + var_dump($stmt->closeCursor()); +}); + +$stmt = $db->prepare('SELECT x FROM t ORDER BY x COLLATE evil'); +$stmt->execute(); +?> +--EXPECT-- +bailing out +bool(true)