Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ PHP NEWS
registrations are freed while still reachable from the cycle collector.
(Ilia Alshanetsky)

- SQLite:
. Fixed a crash when SQLite3::close() is called from a userland callback.
(Ilia Alshanetsky)


24 Sep 2026, PHP 8.4.26

Expand Down
3 changes: 3 additions & 0 deletions ext/sqlite3/php_sqlite3_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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. */
Expand All @@ -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;
Expand Down
51 changes: 51 additions & 0 deletions ext/sqlite3/sqlite3.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -774,13 +778,20 @@ 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;
}

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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
/* }}}*/
Expand Down Expand Up @@ -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)) {
Expand All @@ -938,10 +961,25 @@ 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) {
zend_bailout();
}

if (EG(exception)) {
ret = 0;
Expand Down Expand Up @@ -988,6 +1026,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);
Expand Down Expand Up @@ -1037,6 +1076,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);
Expand Down Expand Up @@ -1085,6 +1125,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);

Expand Down Expand Up @@ -2151,8 +2192,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 {
Expand All @@ -2176,6 +2224,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;
}
/* }}} */
Expand Down
35 changes: 35 additions & 0 deletions ext/sqlite3/tests/sqlite3_close_from_callback.phpt
Original file line number Diff line number Diff line change
@@ -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
Loading