Skip to content

Commit 8adcb3b

Browse files
committed
sqlite: fix crash on db.close() from inside a user function
Calling db.close() from inside a user-defined function callback while sqlite3_step is on the call stack caused two distinct crashes: 1. DatabaseSync::Close ran sqlite3_finalize on the statement whose sqlite3_step frame was still active, freeing the VM that step was executing. The outer step then operated on freed memory. 2. Even if (1) is avoided, StatementExecutionHelper::Run dereferenced db->Connection() via sqlite3_last_insert_rowid / sqlite3_changes64 after step returned. The reentrant close zeroed connection_, so the deref crashed. Add a MarkStepping() RAII guard wrapped around every sqlite3_step caller. If Finalize() is called while stepping_, defer it; the guard's destructor runs the deferred finalize after step returns. Add a connection-null check in StatementExecutionHelper::Run before the connection-dependent reads, throwing ERR_INVALID_STATE. Fixes: #63180 Signed-off-by: Matthew McEachen <matthew@photostructure.com>
1 parent dc99d18 commit 8adcb3b

3 files changed

Lines changed: 119 additions & 3 deletions

File tree

src/node_sqlite.cc

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2563,6 +2563,13 @@ StatementSync::~StatementSync() {
25632563
}
25642564

25652565
void StatementSync::Finalize() {
2566+
if (statement_ == nullptr) {
2567+
return;
2568+
}
2569+
if (stepping_) {
2570+
finalize_pending_ = true;
2571+
return;
2572+
}
25662573
sqlite3_finalize(statement_);
25672574
statement_ = nullptr;
25682575
InvalidateColumnNameCache();
@@ -2888,6 +2895,10 @@ MaybeLocal<Object> StatementExecutionHelper::Run(Environment* env,
28882895
EscapableHandleScope scope(isolate);
28892896
sqlite3_step(stmt);
28902897
int r = sqlite3_reset(stmt);
2898+
if (db->Connection() == nullptr) {
2899+
THROW_ERR_INVALID_STATE(env, "database connection was closed mid-query");
2900+
return MaybeLocal<Object>();
2901+
}
28912902
CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_OK, MaybeLocal<Object>());
28922903

28932904
sqlite3_int64 last_insert_rowid = sqlite3_last_insert_rowid(db->Connection());
@@ -3016,9 +3027,9 @@ void StatementSync::All(const FunctionCallbackInfo<Value>& args) {
30163027
return;
30173028
}
30183029

3019-
auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); });
3020-
30213030
Local<Value> result;
3031+
auto step = stmt->MarkStepping();
3032+
auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); });
30223033
if (StatementExecutionHelper::All(env,
30233034
stmt->db_.get(),
30243035
stmt->statement_,
@@ -3066,6 +3077,7 @@ void StatementSync::Get(const FunctionCallbackInfo<Value>& args) {
30663077
}
30673078

30683079
Local<Value> result;
3080+
auto step = stmt->MarkStepping();
30693081
if (StatementExecutionHelper::Get(env,
30703082
stmt->db_.get(),
30713083
stmt->statement_,
@@ -3090,6 +3102,7 @@ void StatementSync::Run(const FunctionCallbackInfo<Value>& args) {
30903102
}
30913103

30923104
Local<Object> result;
3105+
auto step = stmt->MarkStepping();
30933106
if (StatementExecutionHelper::Run(
30943107
env, stmt->db_.get(), stmt->statement_, stmt->use_big_ints_)
30953108
.ToLocal(&result)) {
@@ -3354,6 +3367,7 @@ void SQLTagStore::Run(const FunctionCallbackInfo<Value>& args) {
33543367
}
33553368

33563369
Local<Object> result;
3370+
auto step = stmt->MarkStepping();
33573371
if (StatementExecutionHelper::Run(
33583372
env, stmt->db_.get(), stmt->statement_, stmt->use_big_ints_)
33593373
.ToLocal(&result)) {
@@ -3425,6 +3439,7 @@ void SQLTagStore::Get(const FunctionCallbackInfo<Value>& args) {
34253439
}
34263440

34273441
Local<Value> result;
3442+
auto step = stmt->MarkStepping();
34283443
if (StatementExecutionHelper::Get(env,
34293444
stmt->db_.get(),
34303445
stmt->statement_,
@@ -3463,8 +3478,9 @@ void SQLTagStore::All(const FunctionCallbackInfo<Value>& args) {
34633478
}
34643479
}
34653480

3466-
auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); });
34673481
Local<Value> result;
3482+
auto step = stmt->MarkStepping();
3483+
auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); });
34683484
if (StatementExecutionHelper::All(env,
34693485
stmt->db_.get(),
34703486
stmt->statement_,
@@ -3691,6 +3707,7 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo<Value>& args) {
36913707
iter->statement_reset_generation_ != iter->stmt_->reset_generation_,
36923708
"iterator was invalidated");
36933709

3710+
auto step = iter->stmt_->MarkStepping();
36943711
int r = sqlite3_step(iter->stmt_->statement_);
36953712
if (r != SQLITE_ROW) {
36963713
CHECK_ERROR_OR_THROW(

src/node_sqlite.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,19 @@ class StatementSync : public BaseObject {
284284
void Finalize();
285285
bool IsFinalized();
286286

287+
// RAII guard: defer Finalize() if called while sqlite3_step is on the
288+
// stack for this statement. Wrap every sqlite3_step caller.
289+
inline auto MarkStepping() {
290+
stepping_ = true;
291+
return OnScopeLeave([this]() {
292+
stepping_ = false;
293+
if (finalize_pending_) {
294+
finalize_pending_ = false;
295+
Finalize();
296+
}
297+
});
298+
}
299+
287300
SET_MEMORY_INFO_NAME(StatementSync)
288301
SET_SELF_SIZE(StatementSync)
289302

@@ -295,6 +308,8 @@ class StatementSync : public BaseObject {
295308
bool use_big_ints_;
296309
bool allow_bare_named_params_;
297310
bool allow_unknown_named_params_;
311+
bool stepping_ = false;
312+
bool finalize_pending_ = false;
298313
uint64_t reset_generation_ = 0;
299314
std::optional<std::map<std::string, std::string>> bare_named_params_;
300315
inline int ResetStatement();

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,4 +411,88 @@ suite('DatabaseSync.prototype.function()', () => {
411411
message: /database is not open/,
412412
});
413413
});
414+
415+
suite('reentrant db.close() from inside a user function callback', () => {
416+
function setup() {
417+
const db = new DatabaseSync(':memory:');
418+
db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)');
419+
db.prepare('INSERT INTO t VALUES (1, 10)').run();
420+
db.prepare('INSERT INTO t VALUES (2, 20)').run();
421+
db.function('close_db', (x) => {
422+
try { db.close(); } catch { /* already closed on later rows */ }
423+
return x;
424+
});
425+
return db;
426+
}
427+
428+
test('.all() completes without crashing', () => {
429+
const db = setup();
430+
const rows = db.prepare('SELECT close_db(v) AS v FROM t').all();
431+
assert.ok(Array.isArray(rows));
432+
});
433+
434+
test('.get() completes without crashing', () => {
435+
const db = setup();
436+
const row = db.prepare('SELECT close_db(v) AS v FROM t').get();
437+
assert.deepStrictEqual(row, { __proto__: null, v: 10 });
438+
});
439+
440+
test('.run() throws ERR_INVALID_STATE rather than crashing', () => {
441+
const db = new DatabaseSync(':memory:');
442+
db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)');
443+
db.function('close_db', (x) => {
444+
try { db.close(); } catch { /* */ }
445+
return x;
446+
});
447+
assert.throws(
448+
() => db.prepare('INSERT INTO t SELECT close_db(1), close_db(2)').run(),
449+
{ code: 'ERR_INVALID_STATE', message: /closed mid-query/ },
450+
);
451+
});
452+
453+
test('iterator returns first row, then throws on next()', () => {
454+
const db = setup();
455+
const iter = db.prepare('SELECT close_db(v) AS v FROM t').iterate();
456+
assert.strictEqual(iter.next().done, false);
457+
assert.throws(() => iter.next(), {
458+
code: 'ERR_INVALID_STATE',
459+
message: /statement has been finalized/,
460+
});
461+
});
462+
463+
test('SQL tag store .run() throws ERR_INVALID_STATE', () => {
464+
const db = new DatabaseSync(':memory:');
465+
db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)');
466+
db.function('close_db', (x) => {
467+
try { db.close(); } catch { /* */ }
468+
return x;
469+
});
470+
const sql = db.createTagStore(4);
471+
assert.throws(
472+
() => sql.run`INSERT INTO t SELECT close_db(${1}), close_db(${2})`,
473+
{ code: 'ERR_INVALID_STATE', message: /closed mid-query/ },
474+
);
475+
});
476+
477+
test('SQL tag store .all() completes without crashing', () => {
478+
const db = setup();
479+
const sql = db.createTagStore(4);
480+
const rows = sql.all`SELECT close_db(v) AS v FROM t`;
481+
assert.ok(Array.isArray(rows));
482+
});
483+
484+
test('SQL tag store .get() completes without crashing', () => {
485+
const db = setup();
486+
const sql = db.createTagStore(4);
487+
const row = sql.get`SELECT close_db(v) AS v FROM t`;
488+
assert.deepStrictEqual(row, { __proto__: null, v: 10 });
489+
});
490+
491+
test('a fresh database works after the reentrant close', () => {
492+
setup().prepare('SELECT close_db(v) FROM t').all();
493+
const db2 = new DatabaseSync(':memory:');
494+
assert.deepStrictEqual(db2.prepare('SELECT 1 AS x').get(),
495+
{ __proto__: null, x: 1 });
496+
});
497+
});
414498
});

0 commit comments

Comments
 (0)