sqlite: reject closing a session from a callback

SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.

Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.

Fixes: https://github.com/nodejs/node/issues/65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
This commit is contained in:
Trevor Burnham authored and Kamat, Trivikram committed 2026-08-30 10:15:09 -07:00
1 parent f7e2c140c8
commit 01c1300dfc
3 files changed
+174 -4

No files matched your search

+14
View File
@@ -169,6 +169,16 @@ inline MaybeLocal<Value> IntegerToValue(Isolate* isolate,
sqlite3_stmt_busy((stmt)->statement_.get()), \
"database cannot be accessed from an authorizer callback")
// SQLite's session module reaches back into JavaScript from inside the
// pre-update hook, while it is still walking the connection's session list and
// reading the table it found there. Deleting a session frees memory that walk
// is still using, so no callback may close one.
#define THROW_AND_RETURN_IF_SESSION_IN_CALLBACK(env, session) \
THROW_AND_RETURN_ON_BAD_STATE( \
(env), \
(session)->database_->IsInCallback(), \
"session cannot be closed while in a callback")
// A statement's virtual machine cannot be reentered while sqlite3_step() is
// running it. Finalizing it frees the VM outright, and re-running it resets the
// VM mid-execution; both are use-after-free rather than merely a contract
@@ -4358,6 +4368,9 @@ void Session::Close(const FunctionCallbackInfo<Value>& args) {
env, session->session_ == nullptr, "session is not open");
THROW_AND_RETURN_ON_BAD_STATE(
env, session->is_generating_changeset_, "session is currently in use");
// Checked last: changeset generation runs the authorizer, so both conditions
// hold in that case and the more specific message above has to win.
THROW_AND_RETURN_IF_SESSION_IN_CALLBACK(env, session);
session->Delete();
}
@@ -4371,6 +4384,7 @@ void Session::Dispose(const FunctionCallbackInfo<Value>& args) {
}
THROW_AND_RETURN_ON_BAD_STATE(
env, session->is_generating_changeset_, "session is currently in use");
THROW_AND_RETURN_IF_SESSION_IN_CALLBACK(env, session);
session->Delete();
}