diff --git a/NEWS b/NEWS index 3346d38ea898..aa3f00b3fe5b 100644 --- a/NEWS +++ b/NEWS @@ -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 diff --git a/ext/sqlite3/php_sqlite3_structs.h b/ext/sqlite3/php_sqlite3_structs.h index 6d445d6642db..43e595affe1b 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 20714c38aada..a24aefdb6443 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,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; @@ -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); @@ -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); @@ -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); @@ -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 { @@ -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; } /* }}} */ 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 000000000000..0b5523717f76 --- /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-- +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