Commit 7300fa520dd for php.net
commit 7300fa520dd38dadf9563a9104aa0036cb864147
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Mon Aug 24 10:36:56 2026 -0400
ext/sqlite3: reject close() from inside a callback
SQLite3::close() called from within a userland function, aggregate,
collation or authorizer callback freed the registered statements and
functions while sqlite3 was still executing, corrupting the active
statement and crashing the request. Track callback re-entry with a
per-database counter shared by all four callback kinds and throw an
Error from close() while it is non-zero; the database stays usable and
can be closed after the query completes.
Closes GH-23650
diff --git a/NEWS b/NEWS
index 9a862b7e0e1..50815738590 100644
--- a/NEWS
+++ b/NEWS
@@ -27,6 +27,10 @@ PHP NEWS
. Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid
column index. (Ilia Alshanetsky)
+- SQLite:
+ . Fixed a crash when SQLite3::close() is called from a userland callback.
+ (Ilia Alshanetsky)
+
- Zip:
. Fixed ZipArchive::extractTo() ignoring files given in a non-list array.
(David Carlier)
diff --git a/ext/sqlite3/php_sqlite3_structs.h b/ext/sqlite3/php_sqlite3_structs.h
index 6d445d6642d..43e595affe1 100644
--- a/ext/sqlite3/php_sqlite3_structs.h
+++ b/ext/sqlite3/php_sqlite3_structs.h
@@ -50,6 +50,7 @@ typedef struct _php_sqlite3_func {
zend_fcall_info_cache func;
zend_fcall_info_cache step;
zend_fcall_info_cache fini;
+ unsigned int *in_callback_ptr;
} php_sqlite3_func;
/* Structure for SQLite collation function */
@@ -58,6 +59,7 @@ typedef struct _php_sqlite3_collation {
const char *collation_name;
zend_fcall_info_cache cmp_func;
+ unsigned int *in_callback_ptr;
} php_sqlite3_collation;
/* Structure for SQLite Database object. */
@@ -69,6 +71,7 @@ typedef struct _php_sqlite3_db_object {
zend_fcall_info_cache authorizer_fcc;
bool exception;
+ unsigned int in_callback;
zend_llist free_list;
zend_object zo;
diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c
index 20714c38aad..93e50b97c7f 100644
--- a/ext/sqlite3/sqlite3.c
+++ b/ext/sqlite3/sqlite3.c
@@ -191,6 +191,10 @@ PHP_METHOD(SQLite3, close)
}
if (db_obj->initialised) {
+ if (db_obj->in_callback) {
+ zend_throw_error(NULL, "Cannot close SQLite3 database while inside a callback");
+ RETURN_THROWS();
+ }
zend_llist_clean(&(db_obj->free_list));
if(db_obj->db) {
errcode = sqlite3_close(db_obj->db);
@@ -774,6 +778,9 @@ static int sqlite3_do_callback(zend_fcall_info_cache *fcc, uint32_t argc, sqlite
uint32_t fake_argc;
zend_result ret = SUCCESS;
php_sqlite3_agg_context *agg_context = NULL;
+ bool bailout = false;
+ php_sqlite3_func *cb_func = (php_sqlite3_func *)sqlite3_user_data(context);
+ unsigned int *in_callback = cb_func ? cb_func->in_callback_ptr : NULL;
if (is_agg) {
is_agg = 2;
@@ -781,6 +788,10 @@ static int sqlite3_do_callback(zend_fcall_info_cache *fcc, uint32_t argc, sqlite
fake_argc = argc + is_agg;
+ if (in_callback) {
+ (*in_callback)++;
+ }
+
/* build up the params */
if (fake_argc) {
zargs = (zval *)safe_emalloc(fake_argc, sizeof(zval), 0);
@@ -823,7 +834,15 @@ static int sqlite3_do_callback(zend_fcall_info_cache *fcc, uint32_t argc, sqlite
}
}
+ zend_try {
zend_call_known_fcc(fcc, &retval, fake_argc, zargs, /* named_params */ NULL);
+ } zend_catch {
+ bailout = true;
+ } zend_end_try();
+
+ if (in_callback) {
+ (*in_callback)--;
+ }
/* clean up the params */
if (is_agg) {
@@ -889,6 +908,9 @@ static int sqlite3_do_callback(zend_fcall_info_cache *fcc, uint32_t argc, sqlite
if (!Z_ISUNDEF(retval)) {
zval_ptr_dtor(&retval);
}
+ if (bailout) {
+ zend_bailout();
+ }
return ret;
}
/* }}}*/
@@ -929,6 +951,7 @@ static int php_sqlite3_callback_compare(void *coll, int a_len, const void *a, in
zval zargs[2];
zval retval;
int ret = 0;
+ bool bailout = false;
// Exception occurred on previous callback. Don't attempt to call function.
if (EG(exception)) {
@@ -938,10 +961,26 @@ static int php_sqlite3_callback_compare(void *coll, int a_len, const void *a, in
ZVAL_STRINGL(&zargs[0], a, a_len);
ZVAL_STRINGL(&zargs[1], b, b_len);
+ if (collation->in_callback_ptr) {
+ (*collation->in_callback_ptr)++;
+ }
+
+ zend_try {
zend_call_known_fcc(&collation->cmp_func, &retval, /* argc */ 2, zargs, /* named_params */ NULL);
+ } zend_catch {
+ bailout = true;
+ } zend_end_try();
+
+ if (collation->in_callback_ptr) {
+ (*collation->in_callback_ptr)--;
+ }
zval_ptr_dtor(&zargs[0]);
zval_ptr_dtor(&zargs[1]);
+ if (bailout) {
+ zval_ptr_dtor(&retval);
+ zend_bailout();
+ }
if (EG(exception)) {
ret = 0;
@@ -988,6 +1027,7 @@ PHP_METHOD(SQLite3, createFunction)
}
func = (php_sqlite3_func *)ecalloc(1, sizeof(*func));
+ func->in_callback_ptr = &db_obj->in_callback;
if (sqlite3_create_function(db_obj->db, sql_func, sql_func_num_args, flags | SQLITE_UTF8, func, php_sqlite3_callback_func, NULL, NULL) == SQLITE_OK) {
func->func_name = estrdup(sql_func);
@@ -1037,6 +1077,7 @@ PHP_METHOD(SQLite3, createAggregate)
}
func = (php_sqlite3_func *)ecalloc(1, sizeof(*func));
+ func->in_callback_ptr = &db_obj->in_callback;
if (sqlite3_create_function(db_obj->db, sql_func, sql_func_num_args, SQLITE_UTF8, func, NULL, php_sqlite3_callback_step, php_sqlite3_callback_final) == SQLITE_OK) {
func->func_name = estrdup(sql_func);
@@ -1085,6 +1126,7 @@ PHP_METHOD(SQLite3, createCollation)
}
collation = (php_sqlite3_collation *)ecalloc(1, sizeof(*collation));
+ collation->in_callback_ptr = &db_obj->in_callback;
if (sqlite3_create_collation(db_obj->db, collation_name, SQLITE_UTF8, collation, php_sqlite3_callback_compare) == SQLITE_OK) {
collation->collation_name = estrdup(collation_name);
@@ -2151,8 +2193,15 @@ static int php_sqlite3_authorizer(void *autharg, int action, const char *arg1, c
}
int authreturn = SQLITE_DENY;
+ bool bailout = false;
+ db_obj->in_callback++;
+ zend_try {
zend_call_known_fcc(&db_obj->authorizer_fcc, &retval, /* argc */ 5, argv, /* named_params */ NULL);
+ } zend_catch {
+ bailout = true;
+ } zend_end_try();
+ db_obj->in_callback--;
if (Z_ISUNDEF(retval)) {
php_sqlite3_error(db_obj, 0, "An error occurred while invoking the authorizer callback");
} else {
@@ -2176,6 +2225,9 @@ static int php_sqlite3_authorizer(void *autharg, int action, const char *arg1, c
zval_ptr_dtor(&argv[3]);
zval_ptr_dtor(&argv[4]);
+ if (bailout) {
+ zend_bailout();
+ }
return authreturn;
}
/* }}} */
diff --git a/ext/sqlite3/tests/sqlite3_close_from_callback.phpt b/ext/sqlite3/tests/sqlite3_close_from_callback.phpt
new file mode 100644
index 00000000000..0b5523717f7
--- /dev/null
+++ b/ext/sqlite3/tests/sqlite3_close_from_callback.phpt
@@ -0,0 +1,35 @@
+--TEST--
+SQLite3::close() from within a UDF callback must not corrupt active statement
+--EXTENSIONS--
+sqlite3
+--FILE--
+<?php
+$db = new SQLite3(':memory:');
+$db->createFunction('boom', function () use ($db) {
+ try {
+ var_dump($db->close());
+ } catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+ }
+ return 1;
+});
+$stmt = $db->prepare('SELECT boom()');
+var_dump($stmt !== false);
+$res = $stmt->execute();
+var_dump($res !== false);
+var_dump($res->fetchArray(SQLITE3_NUM));
+$res->finalize();
+var_dump($db->close());
+echo "done\n";
+?>
+--EXPECT--
+bool(true)
+Error: Cannot close SQLite3 database while inside a callback
+bool(true)
+Error: Cannot close SQLite3 database while inside a callback
+array(1) {
+ [0]=>
+ int(1)
+}
+bool(true)
+done
diff --git a/ext/sqlite3/tests/sqlite3_close_from_callback_aggregate.phpt b/ext/sqlite3/tests/sqlite3_close_from_callback_aggregate.phpt
new file mode 100644
index 00000000000..b5bdb9615a7
--- /dev/null
+++ b/ext/sqlite3/tests/sqlite3_close_from_callback_aggregate.phpt
@@ -0,0 +1,41 @@
+--TEST--
+SQLite3::close() from within a createAggregate() callback must not corrupt active statement
+--EXTENSIONS--
+sqlite3
+--FILE--
+<?php
+$db = new SQLite3(':memory:');
+$db->exec('CREATE TABLE t (a INTEGER)');
+$db->exec('INSERT INTO t VALUES (1)');
+$db->exec('INSERT INTO t VALUES (2)');
+
+$reported = false;
+$db->createAggregate('agg', function ($context, $rows, $a) use (&$db, &$reported) {
+ if (!$reported) {
+ $reported = true;
+ try {
+ var_dump($db->close());
+ } catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+ }
+ }
+ return (int) $context + $a;
+}, function ($context, $rows) {
+ return $context;
+}, 1);
+
+$stmt = $db->prepare('SELECT agg(a) FROM t');
+$res = $stmt->execute();
+var_dump($res->fetchArray(SQLITE3_NUM));
+$res->finalize();
+var_dump($db->close());
+echo "done\n";
+?>
+--EXPECT--
+Error: Cannot close SQLite3 database while inside a callback
+array(1) {
+ [0]=>
+ int(3)
+}
+bool(true)
+done
diff --git a/ext/sqlite3/tests/sqlite3_close_from_callback_authorizer.phpt b/ext/sqlite3/tests/sqlite3_close_from_callback_authorizer.phpt
new file mode 100644
index 00000000000..1d5cf9e4922
--- /dev/null
+++ b/ext/sqlite3/tests/sqlite3_close_from_callback_authorizer.phpt
@@ -0,0 +1,38 @@
+--TEST--
+SQLite3::close() from within a setAuthorizer() callback must not finalize live statements
+--EXTENSIONS--
+sqlite3
+--FILE--
+<?php
+$db = new SQLite3(':memory:');
+$db->exec('CREATE TABLE t (a INTEGER)');
+$db->exec('INSERT INTO t VALUES (7)');
+$live = $db->prepare('SELECT a FROM t');
+
+$reported = false;
+$db->setAuthorizer(function ($action, $arg1, $arg2, $arg3, $arg4) use (&$db, &$reported) {
+ if (!$reported) {
+ $reported = true;
+ try {
+ var_dump($db->close());
+ } catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+ }
+ }
+ return SQLite3::OK;
+});
+
+$db->prepare('SELECT 1');
+
+$res = $live->execute();
+var_dump($res->fetchArray(SQLITE3_NUM));
+$res->finalize();
+echo "done\n";
+?>
+--EXPECT--
+Error: Cannot close SQLite3 database while inside a callback
+array(1) {
+ [0]=>
+ int(7)
+}
+done
diff --git a/ext/sqlite3/tests/sqlite3_close_from_callback_collation.phpt b/ext/sqlite3/tests/sqlite3_close_from_callback_collation.phpt
new file mode 100644
index 00000000000..16906894161
--- /dev/null
+++ b/ext/sqlite3/tests/sqlite3_close_from_callback_collation.phpt
@@ -0,0 +1,39 @@
+--TEST--
+SQLite3::close() from within a createCollation() callback must not corrupt active statement
+--EXTENSIONS--
+sqlite3
+--FILE--
+<?php
+$db = new SQLite3(':memory:');
+$db->exec('CREATE TABLE t (s TEXT)');
+$db->exec("INSERT INTO t VALUES ('b')");
+$db->exec("INSERT INTO t VALUES ('a')");
+
+$reported = false;
+$db->createCollation('CB', function ($x, $y) use (&$db, &$reported) {
+ if (!$reported) {
+ $reported = true;
+ try {
+ var_dump($db->close());
+ } catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+ }
+ }
+ return strcmp($x, $y);
+});
+
+$stmt = $db->prepare('SELECT s FROM t ORDER BY s COLLATE CB');
+$res = $stmt->execute();
+while ($row = $res->fetchArray(SQLITE3_NUM)) {
+ echo $row[0], "\n";
+}
+$res->finalize();
+var_dump($db->close());
+echo "done\n";
+?>
+--EXPECT--
+Error: Cannot close SQLite3 database while inside a callback
+a
+b
+bool(true)
+done