Skip to content

Commit 4af37c6

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 598693b commit 4af37c6

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
@@ -1043,6 +1043,12 @@ void DatabaseSync::UntrackStatement(StatementSync* statement) {
10431043
}
10441044
}
10451045

1046+
int DatabaseSync::Step(sqlite3_stmt* statement) {
1047+
statement_execution_depth_++;
1048+
auto leave = OnScopeLeave([&]() { statement_execution_depth_--; });
1049+
return sqlite3_step(statement);
1050+
}
1051+
10461052
inline bool DatabaseSync::IsOpen() {
10471053
return connection_ != nullptr;
10481054
}
@@ -1434,6 +1440,10 @@ void DatabaseSync::Close(const FunctionCallbackInfo<Value>& args) {
14341440
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
14351441
Environment* env = Environment::GetCurrent(args);
14361442
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1443+
THROW_AND_RETURN_ON_BAD_STATE(
1444+
env,
1445+
db->statement_execution_depth_ != 0,
1446+
"cannot close database while a statement is executing");
14371447
db->FinalizeStatements();
14381448
db->DeleteSessions();
14391449
int r = sqlite3_close_v2(db->connection_);
@@ -1829,6 +1839,10 @@ void DatabaseSync::Deserialize(const FunctionCallbackInfo<Value>& args) {
18291839
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
18301840
Environment* env = Environment::GetCurrent(args);
18311841
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1842+
THROW_AND_RETURN_ON_BAD_STATE(
1843+
env,
1844+
db->statement_execution_depth_ != 0,
1845+
"cannot deserialize while a statement is executing");
18321846

18331847
if (!args[0]->IsUint8Array()) {
18341848
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
@@ -2889,7 +2903,7 @@ MaybeLocal<Value> StatementExecutionHelper::All(Environment* env,
28892903
LocalVector<Value> row_values(isolate);
28902904
LocalVector<Name> row_keys(isolate);
28912905

2892-
while ((r = sqlite3_step(stmt)) == SQLITE_ROW) {
2906+
while ((r = db->Step(stmt)) == SQLITE_ROW) {
28932907
if (num_cols == 0) {
28942908
num_cols = sqlite3_column_count(stmt);
28952909
}
@@ -2931,7 +2945,7 @@ MaybeLocal<Object> StatementExecutionHelper::Run(Environment* env,
29312945
bool use_big_ints) {
29322946
Isolate* isolate = env->isolate();
29332947
EscapableHandleScope scope(isolate);
2934-
sqlite3_step(stmt);
2948+
db->Step(stmt);
29352949
int r = sqlite3_reset(stmt);
29362950
CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_OK, MaybeLocal<Object>());
29372951

@@ -3009,7 +3023,7 @@ MaybeLocal<Value> StatementExecutionHelper::Get(Environment* env,
30093023
EscapableHandleScope scope(isolate);
30103024
auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt); });
30113025

3012-
int r = sqlite3_step(stmt);
3026+
int r = db->Step(stmt);
30133027
if (r == SQLITE_DONE) return scope.Escape(Undefined(isolate));
30143028
if (r != SQLITE_ROW) {
30153029
THROW_ERR_SQLITE_ERROR(isolate, db);
@@ -3736,7 +3750,7 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo<Value>& args) {
37363750
iter->statement_reset_generation_ != iter->stmt_->reset_generation_,
37373751
"iterator was invalidated");
37383752

3739-
int r = sqlite3_step(iter->stmt_->statement_);
3753+
int r = iter->stmt_->db_->Step(iter->stmt_->statement_);
37403754
if (r != SQLITE_ROW) {
37413755
CHECK_ERROR_OR_THROW(
37423756
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
@@ -234,13 +234,15 @@ class DatabaseSync : public BaseObject {
234234
private:
235235
bool Open();
236236
void DeleteSessions();
237+
int Step(sqlite3_stmt* statement);
237238

238239
~DatabaseSync() override;
239240
DatabaseOpenConfiguration open_config_;
240241
bool allow_load_extension_;
241242
bool enable_load_extension_;
242243
sqlite3* connection_;
243244
bool ignore_next_sqlite_error_;
245+
uint32_t statement_execution_depth_ = 0;
244246

245247
std::set<BackupJob*> backups_;
246248
std::set<sqlite3_session*> sessions_;
@@ -250,6 +252,7 @@ class DatabaseSync : public BaseObject {
250252
friend class Session;
251253
friend class SQLTagStore;
252254
friend class StatementExecutionHelper;
255+
friend class StatementSyncIterator;
253256
};
254257

255258
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)