Skip to content

Commit 61e44df

Browse files
committed
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
1 parent c8fa0b1 commit 61e44df

4 files changed

Lines changed: 51 additions & 4 deletions

File tree

src/node_sqlite.cc

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,12 @@ void DatabaseSync::UntrackStatement(StatementSync* statement) {
10631063
}
10641064
}
10651065

1066+
int DatabaseSync::Step(sqlite3_stmt* statement) {
1067+
statement_execution_depth_++;
1068+
auto leave = OnScopeLeave([&]() { statement_execution_depth_--; });
1069+
return sqlite3_step(statement);
1070+
}
1071+
10661072
inline bool DatabaseSync::IsOpen() {
10671073
return connection_ != nullptr;
10681074
}
@@ -1456,6 +1462,10 @@ void DatabaseSync::Close(const FunctionCallbackInfo<Value>& args) {
14561462
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
14571463
THROW_AND_RETURN_ON_BAD_STATE(
14581464
env, db->IsInCallback(), "database cannot be closed while in a callback");
1465+
THROW_AND_RETURN_ON_BAD_STATE(
1466+
env,
1467+
db->statement_execution_depth_ != 0,
1468+
"cannot close database while a statement is executing");
14591469
db->FinalizeStatements();
14601470
db->DeleteSessions();
14611471
int r = sqlite3_close_v2(db->connection_);
@@ -1851,6 +1861,10 @@ void DatabaseSync::Deserialize(const FunctionCallbackInfo<Value>& args) {
18511861
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
18521862
Environment* env = Environment::GetCurrent(args);
18531863
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1864+
THROW_AND_RETURN_ON_BAD_STATE(
1865+
env,
1866+
db->statement_execution_depth_ != 0,
1867+
"cannot deserialize while a statement is executing");
18541868

18551869
if (!args[0]->IsUint8Array()) {
18561870
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
@@ -2925,7 +2939,7 @@ MaybeLocal<Value> StatementExecutionHelper::All(Environment* env,
29252939
LocalVector<Value> row_values(isolate);
29262940
LocalVector<Name> row_keys(isolate);
29272941

2928-
while ((r = sqlite3_step(stmt)) == SQLITE_ROW) {
2942+
while ((r = db->Step(stmt)) == SQLITE_ROW) {
29292943
if (num_cols == 0) {
29302944
num_cols = sqlite3_column_count(stmt);
29312945
}
@@ -2967,7 +2981,7 @@ MaybeLocal<Object> StatementExecutionHelper::Run(Environment* env,
29672981
bool use_big_ints) {
29682982
Isolate* isolate = env->isolate();
29692983
EscapableHandleScope scope(isolate);
2970-
sqlite3_step(stmt);
2984+
db->Step(stmt);
29712985
int r = sqlite3_reset(stmt);
29722986
CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_OK, MaybeLocal<Object>());
29732987

@@ -3045,7 +3059,7 @@ MaybeLocal<Value> StatementExecutionHelper::Get(Environment* env,
30453059
EscapableHandleScope scope(isolate);
30463060
auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt); });
30473061

3048-
int r = sqlite3_step(stmt);
3062+
int r = db->Step(stmt);
30493063
if (r == SQLITE_DONE) return scope.Escape(Undefined(isolate));
30503064
if (r != SQLITE_ROW) {
30513065
THROW_ERR_SQLITE_ERROR(isolate, db);
@@ -3772,7 +3786,7 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo<Value>& args) {
37723786
iter->statement_reset_generation_ != iter->stmt_->reset_generation_,
37733787
"iterator was invalidated");
37743788

3775-
int r = sqlite3_step(iter->stmt_->statement_);
3789+
int r = iter->stmt_->db_->Step(iter->stmt_->statement_);
37763790
if (r != SQLITE_ROW) {
37773791
CHECK_ERROR_OR_THROW(
37783792
env->isolate(), iter->stmt_->db_.get(), r, SQLITE_DONE, void());

src/node_sqlite.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ class DatabaseSync : public BaseObject {
239239
private:
240240
bool Open();
241241
void DeleteSessions();
242+
int Step(sqlite3_stmt* statement);
242243

243244
~DatabaseSync() override;
244245
DatabaseOpenConfiguration open_config_;
@@ -247,6 +248,7 @@ class DatabaseSync : public BaseObject {
247248
sqlite3* connection_;
248249
bool ignore_next_sqlite_error_;
249250
int callback_depth_ = 0;
251+
uint32_t statement_execution_depth_ = 0;
250252

251253
std::set<BackupJob*> backups_;
252254
std::unordered_set<Session*> sessions_;
@@ -256,6 +258,7 @@ class DatabaseSync : public BaseObject {
256258
friend class Session;
257259
friend class SQLTagStore;
258260
friend class StatementExecutionHelper;
261+
friend class StatementSyncIterator;
259262
};
260263

261264
class StatementSync : public BaseObject {

test/parallel/test-sqlite-custom-functions.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ const { DatabaseSync } = require('node:sqlite');
66
const { suite, test } = require('node:test');
77

88
suite('DatabaseSync.prototype.function()', () => {
9+
test('cannot close the database while a statement is executing', (t) => {
10+
const db = new DatabaseSync(':memory:');
11+
t.after(() => db.close());
12+
db.function('close_database', () => db.close());
13+
const stmt = db.prepare('SELECT close_database()');
14+
15+
t.assert.throws(() => stmt.get(), {
16+
code: 'ERR_INVALID_STATE',
17+
message: 'cannot close database while a statement is executing',
18+
});
19+
t.assert.strictEqual(db.isOpen, true);
20+
});
21+
922
suite('input validation', () => {
1023
const db = new DatabaseSync(':memory:');
1124

test/parallel/test-sqlite-serialize.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,23 @@ suite('DatabaseSync.prototype.serialize()', () => {
8282
});
8383

8484
suite('DatabaseSync.prototype.deserialize()', () => {
85+
test('cannot deserialize while a statement is executing', (t) => {
86+
const source = new DatabaseSync(':memory:');
87+
const serialized = source.serialize();
88+
source.close();
89+
90+
const db = new DatabaseSync(':memory:');
91+
t.after(() => db.close());
92+
db.function('deserialize_database', () => db.deserialize(serialized));
93+
const stmt = db.prepare('SELECT deserialize_database()');
94+
95+
t.assert.throws(() => stmt.get(), {
96+
code: 'ERR_INVALID_STATE',
97+
message: 'cannot deserialize while a statement is executing',
98+
});
99+
t.assert.strictEqual(db.isOpen, true);
100+
});
101+
85102
test('loads a serialized database', (t) => {
86103
const db1 = new DatabaseSync(':memory:');
87104
db1.exec('CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT)');

0 commit comments

Comments
 (0)