From c11abfd9514a527aa32e44cfe56c018980b481f3 Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:45:54 -0700 Subject: [PATCH] sqlite: prevent reentrant statement finalization Track statement execution depth while calling sqlite3_step(). Reject close() and deserialize() while a statement is executing so callbacks cannot finalize the active statement and crash. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol --- src/node_sqlite.cc | 22 +++++++++++++++---- src/node_sqlite.h | 3 +++ test/parallel/test-sqlite-custom-functions.js | 13 +++++++++++ test/parallel/test-sqlite-serialize.js | 17 ++++++++++++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 8e2f13cd633439..37e628002fd8c2 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -1043,6 +1043,12 @@ void DatabaseSync::UntrackStatement(StatementSync* statement) { } } +int DatabaseSync::Step(sqlite3_stmt* statement) { + statement_execution_depth_++; + auto leave = OnScopeLeave([&]() { statement_execution_depth_--; }); + return sqlite3_step(statement); +} + inline bool DatabaseSync::IsOpen() { return connection_ != nullptr; } @@ -1434,6 +1440,10 @@ void DatabaseSync::Close(const FunctionCallbackInfo& args) { ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); + THROW_AND_RETURN_ON_BAD_STATE( + env, + db->statement_execution_depth_ != 0, + "cannot close database while a statement is executing"); db->FinalizeStatements(); db->DeleteSessions(); int r = sqlite3_close_v2(db->connection_); @@ -1829,6 +1839,10 @@ void DatabaseSync::Deserialize(const FunctionCallbackInfo& args) { ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); + THROW_AND_RETURN_ON_BAD_STATE( + env, + db->statement_execution_depth_ != 0, + "cannot deserialize while a statement is executing"); if (!args[0]->IsUint8Array()) { THROW_ERR_INVALID_ARG_TYPE(env->isolate(), @@ -2884,7 +2898,7 @@ MaybeLocal StatementExecutionHelper::All(Environment* env, LocalVector row_values(isolate); LocalVector row_keys(isolate); - while ((r = sqlite3_step(stmt)) == SQLITE_ROW) { + while ((r = db->Step(stmt)) == SQLITE_ROW) { if (num_cols == 0) { num_cols = sqlite3_column_count(stmt); } @@ -2926,7 +2940,7 @@ MaybeLocal StatementExecutionHelper::Run(Environment* env, bool use_big_ints) { Isolate* isolate = env->isolate(); EscapableHandleScope scope(isolate); - sqlite3_step(stmt); + db->Step(stmt); int r = sqlite3_reset(stmt); CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_OK, MaybeLocal()); @@ -3004,7 +3018,7 @@ MaybeLocal StatementExecutionHelper::Get(Environment* env, EscapableHandleScope scope(isolate); auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt); }); - int r = sqlite3_step(stmt); + int r = db->Step(stmt); if (r == SQLITE_DONE) return scope.Escape(Undefined(isolate)); if (r != SQLITE_ROW) { THROW_ERR_SQLITE_ERROR(isolate, db); @@ -3731,7 +3745,7 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo& args) { iter->statement_reset_generation_ != iter->stmt_->reset_generation_, "iterator was invalidated"); - int r = sqlite3_step(iter->stmt_->statement_); + int r = iter->stmt_->db_->Step(iter->stmt_->statement_); if (r != SQLITE_ROW) { CHECK_ERROR_OR_THROW( env->isolate(), iter->stmt_->db_.get(), r, SQLITE_DONE, void()); diff --git a/src/node_sqlite.h b/src/node_sqlite.h index 84c0e26e61ad8f..ed68c9617d6233 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -234,6 +234,7 @@ class DatabaseSync : public BaseObject { private: bool Open(); void DeleteSessions(); + int Step(sqlite3_stmt* statement); ~DatabaseSync() override; DatabaseOpenConfiguration open_config_; @@ -241,6 +242,7 @@ class DatabaseSync : public BaseObject { bool enable_load_extension_; sqlite3* connection_; bool ignore_next_sqlite_error_; + uint32_t statement_execution_depth_ = 0; std::set backups_; std::set sessions_; @@ -250,6 +252,7 @@ class DatabaseSync : public BaseObject { friend class Session; friend class SQLTagStore; friend class StatementExecutionHelper; + friend class StatementSyncIterator; }; class StatementSync : public BaseObject { diff --git a/test/parallel/test-sqlite-custom-functions.js b/test/parallel/test-sqlite-custom-functions.js index 6b5f974ede893e..938fabd8b699a1 100644 --- a/test/parallel/test-sqlite-custom-functions.js +++ b/test/parallel/test-sqlite-custom-functions.js @@ -6,6 +6,19 @@ const { DatabaseSync } = require('node:sqlite'); const { suite, test } = require('node:test'); suite('DatabaseSync.prototype.function()', () => { + test('cannot close the database while a statement is executing', (t) => { + const db = new DatabaseSync(':memory:'); + t.after(() => db.close()); + db.function('close_database', () => db.close()); + const stmt = db.prepare('SELECT close_database()'); + + t.assert.throws(() => stmt.get(), { + code: 'ERR_INVALID_STATE', + message: 'cannot close database while a statement is executing', + }); + t.assert.strictEqual(db.isOpen, true); + }); + suite('input validation', () => { const db = new DatabaseSync(':memory:'); diff --git a/test/parallel/test-sqlite-serialize.js b/test/parallel/test-sqlite-serialize.js index 77b9d9c5f483a3..17461069015f8b 100644 --- a/test/parallel/test-sqlite-serialize.js +++ b/test/parallel/test-sqlite-serialize.js @@ -82,6 +82,23 @@ suite('DatabaseSync.prototype.serialize()', () => { }); suite('DatabaseSync.prototype.deserialize()', () => { + test('cannot deserialize while a statement is executing', (t) => { + const source = new DatabaseSync(':memory:'); + const serialized = source.serialize(); + source.close(); + + const db = new DatabaseSync(':memory:'); + t.after(() => db.close()); + db.function('deserialize_database', () => db.deserialize(serialized)); + const stmt = db.prepare('SELECT deserialize_database()'); + + t.assert.throws(() => stmt.get(), { + code: 'ERR_INVALID_STATE', + message: 'cannot deserialize while a statement is executing', + }); + t.assert.strictEqual(db.isOpen, true); + }); + test('loads a serialized database', (t) => { const db1 = new DatabaseSync(':memory:'); db1.exec('CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT)');