mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
sqlite: reject connection access from authorizer callbacks
SQLite requires that an authorizer callback not modify the connection that invoked it, and counts sqlite3_prepare_v2() and sqlite3_step() as modifications. node:sqlite let the callback call prepare(), exec(), the statement execution methods, and other connection-mutating APIs on the same DatabaseSync. Track authorizer depth on DatabaseSync with an RAII guard around the callback and throw ERR_INVALID_STATE from the affected entry points while it is on the stack. Covering every authorizer invocation, including the re-prepare that SQLite can run during sqlite3_step(), exposed a second and distinct hazard: reentering a statement that is currently being stepped is a use-after-free rather than a contract violation, since finalizing it frees the virtual machine under sqlite3_step() and re-running it resets that machine mid-execution. Any callback SQLite invokes during execution can reach it, so a user-defined function is enough. Track the statements currently being stepped and reject reentry into only those, which leaves a user-defined function free to prepare, run, and finalize its own helper statements. Signed-off-by: Trevor Burnham <trevorburnham@gmail.com> Fixes: https://github.com/nodejs/node/issues/63207 Assisted-by: claude:opus-5 PR-URL: https://github.com/nodejs/node/pull/65156 Reviewed-By: Xuguang Mei <meixuguang@gmail.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
This commit is contained in:
1 parent
bc813a7a6d
commit
c4b755ee00
5 files changed
+711
-7
No files matched your search
@@ -442,6 +442,11 @@ wrapper around [`sqlite3_create_function_v2()`][].
|
||||
|
||||
<!-- YAML
|
||||
added: v24.10.0
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/65156
|
||||
description: Accessing the invoking database connection from the authorizer
|
||||
callback now throws.
|
||||
-->
|
||||
|
||||
* `callback` {Function|null} The authorizer function to set, or `null` to
|
||||
@@ -467,6 +472,31 @@ The callback must return one of the following constants:
|
||||
* `SQLITE_DENY` - Deny the operation (causes an error).
|
||||
* `SQLITE_IGNORE` - Ignore the operation (silently skip).
|
||||
|
||||
SQLite requires that the authorizer callback not modify the database connection
|
||||
that invoked it, which includes preparing and stepping statements. Methods that
|
||||
would do so throw an error with code `ERR_INVALID_STATE` while the callback is
|
||||
on the stack, including `database.prepare()`, `database.exec()`, the execution
|
||||
methods of that connection's statements, iterators, and tag stores, and
|
||||
`database.setAuthorizer()` itself. Other connections remain usable.
|
||||
|
||||
The callback can also be invoked from within `statement.run()`,
|
||||
`statement.get()`, and similar methods, because SQLite may re-prepare a
|
||||
statement during execution after a schema change.
|
||||
|
||||
Separately, a statement that is currently being executed cannot be reentered.
|
||||
Calling `statement.close()` on it would free the virtual machine that is
|
||||
running, and re-running it through `statement.run()`, `statement.get()`,
|
||||
`statement.all()`, `statement.iterate()`, `iterator.next()`,
|
||||
`iterator.return()`, or the equivalent tag store methods would reset that
|
||||
virtual machine mid-execution. All of these throw an `ERR_INVALID_STATE` error
|
||||
instead. This applies to any callback SQLite invokes during execution, such as a
|
||||
user-defined function. Other statements on the connection remain usable.
|
||||
|
||||
Operations that touch no SQLite state stay available from the callback:
|
||||
`sqlTagStore.clear()`, which only drops cached statements, and `next()` and
|
||||
`return()` on an already-drained iterator, which keep returning
|
||||
`{ done: true }`.
|
||||
|
||||
```cjs
|
||||
const { DatabaseSync, constants } = require('node:sqlite');
|
||||
const db = new DatabaseSync(':memory:');
|
||||
|
||||
+101
-5
@@ -107,6 +107,27 @@ inline MaybeLocal<String> Utf8StringMaybeOneByte(Isolate* isolate,
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// SQLite requires that an authorizer callback not modify the connection that
|
||||
// invoked it. Preparing and stepping statements both count as modifying it.
|
||||
// See https://www.sqlite.org/c3ref/set_authorizer.html.
|
||||
#define THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db) \
|
||||
THROW_AND_RETURN_ON_BAD_STATE( \
|
||||
(env), \
|
||||
(db)->IsInAuthorizerCallback(), \
|
||||
"database cannot be accessed from an authorizer 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
|
||||
// violation. Callbacks that SQLite invokes during execution are therefore
|
||||
// barred from reaching the statement being stepped, though other statements on
|
||||
// the connection stay usable.
|
||||
#define THROW_AND_RETURN_IF_STEPPING(env, stmt) \
|
||||
THROW_AND_RETURN_ON_BAD_STATE( \
|
||||
(env), \
|
||||
(stmt)->db_->IsSteppingStatement((stmt)->statement_.get()), \
|
||||
"statement is already being executed")
|
||||
|
||||
#define SQLITE_VALUE_TO_JS(from, isolate, use_big_int_args, result, ...) \
|
||||
do { \
|
||||
switch (sqlite3_##from##_type(__VA_ARGS__)) { \
|
||||
@@ -840,6 +861,12 @@ Intercepted DatabaseSyncLimits::LimitsSetter(
|
||||
return Intercepted::kYes;
|
||||
}
|
||||
|
||||
if (limits->database_->IsInAuthorizerCallback()) {
|
||||
THROW_ERR_INVALID_STATE(
|
||||
env, "database cannot be accessed from an authorizer callback");
|
||||
return Intercepted::kYes;
|
||||
}
|
||||
|
||||
if (!value->IsNumber()) {
|
||||
THROW_ERR_INVALID_ARG_TYPE(
|
||||
isolate, "Limit value must be a non-negative integer or Infinity.");
|
||||
@@ -1097,6 +1124,7 @@ void DatabaseSync::CreateTagStore(const FunctionCallbackInfo<Value>& args) {
|
||||
THROW_ERR_INVALID_STATE(env, "database is not open");
|
||||
return;
|
||||
}
|
||||
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
|
||||
int capacity = 1000;
|
||||
if (args.Length() > 0 && !args[0]->IsUndefined()) {
|
||||
if (!args[0]->IsNumber()) {
|
||||
@@ -1499,6 +1527,7 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& 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_IF_IN_AUTHORIZER(env, db);
|
||||
|
||||
if (!args[0]->IsString()) {
|
||||
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
|
||||
@@ -1636,6 +1665,7 @@ void DatabaseSync::Exec(const FunctionCallbackInfo<Value>& 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_IF_IN_AUTHORIZER(env, db);
|
||||
|
||||
if (!args[0]->IsString()) {
|
||||
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
|
||||
@@ -1660,6 +1690,7 @@ void DatabaseSync::CustomFunction(const FunctionCallbackInfo<Value>& 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_IF_IN_AUTHORIZER(env, db);
|
||||
|
||||
if (!args[0]->IsString()) {
|
||||
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
|
||||
@@ -1833,6 +1864,7 @@ void DatabaseSync::Serialize(const FunctionCallbackInfo<Value>& 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_IF_IN_AUTHORIZER(env, db);
|
||||
|
||||
std::string db_name = "main";
|
||||
if (!args[0]->IsUndefined()) {
|
||||
@@ -1967,6 +1999,7 @@ void DatabaseSync::AggregateFunction(const FunctionCallbackInfo<Value>& 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_IF_IN_AUTHORIZER(env, db);
|
||||
Utf8Value name(env->isolate(), args[0].As<String>());
|
||||
Local<Object> options = args[1].As<Object>();
|
||||
Local<Value> start_v;
|
||||
@@ -2178,6 +2211,7 @@ void DatabaseSync::CreateSession(const FunctionCallbackInfo<Value>& args) {
|
||||
DatabaseSync* db;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
|
||||
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
|
||||
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
|
||||
|
||||
sqlite3_session* pSession;
|
||||
int r = sqlite3session_create(db->connection_, db_name.c_str(), &pSession);
|
||||
@@ -2347,6 +2381,7 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo<Value>& 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_IF_IN_AUTHORIZER(env, db);
|
||||
|
||||
if (!args[0]->IsUint8Array()) {
|
||||
THROW_ERR_INVALID_ARG_TYPE(
|
||||
@@ -2482,6 +2517,7 @@ void DatabaseSync::EnableLoadExtension(
|
||||
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_IF_IN_AUTHORIZER(env, db);
|
||||
|
||||
Isolate* isolate = env->isolate();
|
||||
if (!args[0]->IsBoolean()) {
|
||||
@@ -2510,6 +2546,7 @@ void DatabaseSync::EnableDefensive(const FunctionCallbackInfo<Value>& 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_IF_IN_AUTHORIZER(env, db);
|
||||
|
||||
Isolate* isolate = env->isolate();
|
||||
if (!args[0]->IsBoolean()) {
|
||||
@@ -2535,6 +2572,7 @@ void DatabaseSync::LoadExtension(const FunctionCallbackInfo<Value>& args) {
|
||||
env, !db->allow_load_extension_, "extension loading is not allowed");
|
||||
THROW_AND_RETURN_ON_BAD_STATE(
|
||||
env, !db->enable_load_extension_, "extension loading is not allowed");
|
||||
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
|
||||
|
||||
if (!args[0]->IsString()) {
|
||||
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
|
||||
@@ -2563,6 +2601,7 @@ void DatabaseSync::SetAuthorizer(const FunctionCallbackInfo<Value>& 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_IF_IN_AUTHORIZER(env, db);
|
||||
|
||||
Isolate* isolate = env->isolate();
|
||||
|
||||
@@ -2599,6 +2638,7 @@ int DatabaseSync::AuthorizerCallback(void* user_data,
|
||||
const char* param4) {
|
||||
DatabaseSync* db = static_cast<DatabaseSync*>(user_data);
|
||||
CallbackDepthGuard guard(db);
|
||||
AuthorizerDepthGuard authorizer_guard(db);
|
||||
Environment* env = db->env();
|
||||
Isolate* isolate = env->isolate();
|
||||
HandleScope handle_scope(isolate);
|
||||
@@ -2712,12 +2752,20 @@ void StatementSync::Close(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
THROW_AND_RETURN_ON_BAD_STATE(
|
||||
env, stmt->IsFinalized(), "statement has been finalized");
|
||||
THROW_AND_RETURN_IF_STEPPING(env, stmt);
|
||||
stmt->Close();
|
||||
}
|
||||
|
||||
void StatementSync::Dispose(const FunctionCallbackInfo<Value>& args) {
|
||||
StatementSync* stmt;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
// Disposal is idempotent, so an already-finalized statement is a no-op even
|
||||
// inside a callback.
|
||||
if (stmt->IsFinalized()) {
|
||||
return;
|
||||
}
|
||||
THROW_AND_RETURN_IF_STEPPING(env, stmt);
|
||||
stmt->Close();
|
||||
}
|
||||
|
||||
@@ -2999,6 +3047,7 @@ MaybeLocal<Value> StatementExecutionHelper::All(Environment* env,
|
||||
LocalVector<Value> row_values(isolate);
|
||||
LocalVector<Name> row_keys(isolate);
|
||||
|
||||
SteppingStatementGuard stepping(db, stmt);
|
||||
while ((r = sqlite3_step(stmt)) == SQLITE_ROW) {
|
||||
if (num_cols == 0) {
|
||||
num_cols = sqlite3_column_count(stmt);
|
||||
@@ -3041,6 +3090,9 @@ MaybeLocal<Object> StatementExecutionHelper::Run(Environment* env,
|
||||
bool use_big_ints) {
|
||||
Isolate* isolate = env->isolate();
|
||||
EscapableHandleScope scope(isolate);
|
||||
// Declared before the reset below so that it outlives it: sqlite3_reset()
|
||||
// can run JavaScript through an aggregate's xFinal.
|
||||
SteppingStatementGuard stepping(db, stmt);
|
||||
bool needs_reset = true;
|
||||
auto reset = OnScopeLeave([&]() {
|
||||
if (needs_reset) sqlite3_reset(stmt);
|
||||
@@ -3128,6 +3180,9 @@ MaybeLocal<Value> StatementExecutionHelper::Get(Environment* env,
|
||||
bool use_big_ints) {
|
||||
Isolate* isolate = env->isolate();
|
||||
EscapableHandleScope scope(isolate);
|
||||
// Declared before the reset below so that it outlives it: sqlite3_reset()
|
||||
// can run JavaScript through an aggregate's xFinal.
|
||||
SteppingStatementGuard stepping(db, stmt);
|
||||
bool needs_reset = true;
|
||||
auto reset = OnScopeLeave([&]() {
|
||||
if (needs_reset) sqlite3_reset(stmt);
|
||||
@@ -3184,6 +3239,8 @@ void StatementSync::All(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
THROW_AND_RETURN_ON_BAD_STATE(
|
||||
env, stmt->IsFinalized(), "statement has been finalized");
|
||||
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, stmt->db_.get());
|
||||
THROW_AND_RETURN_IF_STEPPING(env, stmt);
|
||||
Isolate* isolate = env->isolate();
|
||||
int r = stmt->ResetStatement();
|
||||
CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_OK, void());
|
||||
@@ -3215,6 +3272,8 @@ void StatementSync::Iterate(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
THROW_AND_RETURN_ON_BAD_STATE(
|
||||
env, stmt->IsFinalized(), "statement has been finalized");
|
||||
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, stmt->db_.get());
|
||||
THROW_AND_RETURN_IF_STEPPING(env, stmt);
|
||||
int r = stmt->ResetStatement();
|
||||
CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void());
|
||||
|
||||
@@ -3238,6 +3297,8 @@ void StatementSync::Get(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
THROW_AND_RETURN_ON_BAD_STATE(
|
||||
env, stmt->IsFinalized(), "statement has been finalized");
|
||||
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, stmt->db_.get());
|
||||
THROW_AND_RETURN_IF_STEPPING(env, stmt);
|
||||
int r = stmt->ResetStatement();
|
||||
CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void());
|
||||
|
||||
@@ -3262,6 +3323,8 @@ void StatementSync::Run(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
THROW_AND_RETURN_ON_BAD_STATE(
|
||||
env, stmt->IsFinalized(), "statement has been finalized");
|
||||
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, stmt->db_.get());
|
||||
THROW_AND_RETURN_IF_STEPPING(env, stmt);
|
||||
int r = stmt->ResetStatement();
|
||||
CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void());
|
||||
|
||||
@@ -3598,6 +3661,7 @@ void SQLTagStore::Run(const FunctionCallbackInfo<Value>& args) {
|
||||
|
||||
THROW_AND_RETURN_ON_BAD_STATE(
|
||||
env, !session->database_->IsOpen(), "database is not open");
|
||||
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, session->database_.get());
|
||||
|
||||
BaseObjectPtr<StatementSync> stmt = PrepareStatement(args);
|
||||
|
||||
@@ -3605,6 +3669,8 @@ void SQLTagStore::Run(const FunctionCallbackInfo<Value>& args) {
|
||||
return;
|
||||
}
|
||||
|
||||
THROW_AND_RETURN_IF_STEPPING(env, stmt.get());
|
||||
|
||||
if (!ResetAndBindStatement(env, stmt.get(), args)) {
|
||||
return;
|
||||
}
|
||||
@@ -3624,6 +3690,7 @@ void SQLTagStore::Iterate(const FunctionCallbackInfo<Value>& args) {
|
||||
|
||||
THROW_AND_RETURN_ON_BAD_STATE(
|
||||
env, !session->database_->IsOpen(), "database is not open");
|
||||
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, session->database_.get());
|
||||
|
||||
BaseObjectPtr<StatementSync> stmt = PrepareStatement(args);
|
||||
|
||||
@@ -3631,6 +3698,8 @@ void SQLTagStore::Iterate(const FunctionCallbackInfo<Value>& args) {
|
||||
return;
|
||||
}
|
||||
|
||||
THROW_AND_RETURN_IF_STEPPING(env, stmt.get());
|
||||
|
||||
if (!ResetAndBindStatement(env, stmt.get(), args)) {
|
||||
return;
|
||||
}
|
||||
@@ -3652,6 +3721,7 @@ void SQLTagStore::Get(const FunctionCallbackInfo<Value>& args) {
|
||||
|
||||
THROW_AND_RETURN_ON_BAD_STATE(
|
||||
env, !session->database_->IsOpen(), "database is not open");
|
||||
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, session->database_.get());
|
||||
|
||||
BaseObjectPtr<StatementSync> stmt = PrepareStatement(args);
|
||||
|
||||
@@ -3659,6 +3729,8 @@ void SQLTagStore::Get(const FunctionCallbackInfo<Value>& args) {
|
||||
return;
|
||||
}
|
||||
|
||||
THROW_AND_RETURN_IF_STEPPING(env, stmt.get());
|
||||
|
||||
if (!ResetAndBindStatement(env, stmt.get(), args)) {
|
||||
return;
|
||||
}
|
||||
@@ -3681,6 +3753,7 @@ void SQLTagStore::All(const FunctionCallbackInfo<Value>& args) {
|
||||
|
||||
THROW_AND_RETURN_ON_BAD_STATE(
|
||||
env, !session->database_->IsOpen(), "database is not open");
|
||||
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, session->database_.get());
|
||||
|
||||
BaseObjectPtr<StatementSync> stmt = PrepareStatement(args);
|
||||
|
||||
@@ -3688,6 +3761,8 @@ void SQLTagStore::All(const FunctionCallbackInfo<Value>& args) {
|
||||
return;
|
||||
}
|
||||
|
||||
THROW_AND_RETURN_IF_STEPPING(env, stmt.get());
|
||||
|
||||
if (!ResetAndBindStatement(env, stmt.get(), args)) {
|
||||
return;
|
||||
}
|
||||
@@ -3713,6 +3788,10 @@ void SQLTagStore::All(const FunctionCallbackInfo<Value>& args) {
|
||||
void SQLTagStore::Clear(const FunctionCallbackInfo<Value>& args) {
|
||||
SQLTagStore* store;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&store, args.This());
|
||||
// Clearing the cache drops strong references to the cached statements but
|
||||
// never finalizes one synchronously, so it touches no SQLite state and stays
|
||||
// available from a callback. Invalidating the cache after a schema change is
|
||||
// a legitimate use of an authorizer.
|
||||
store->sql_tags_.Clear();
|
||||
}
|
||||
|
||||
@@ -3922,6 +4001,8 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo<Value>& args) {
|
||||
|
||||
auto iter_template = getLazyIterTemplate(env);
|
||||
|
||||
// A drained iterator touches no SQLite state, so it stays usable from a
|
||||
// callback and is checked before the guards below.
|
||||
if (iter->done_) {
|
||||
MaybeLocal<Value> values[]{
|
||||
Boolean::New(isolate, true),
|
||||
@@ -3935,11 +4016,18 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo<Value>& args) {
|
||||
return;
|
||||
}
|
||||
|
||||
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, iter->stmt_->db_.get());
|
||||
THROW_AND_RETURN_IF_STEPPING(env, iter->stmt_.get());
|
||||
|
||||
THROW_AND_RETURN_ON_BAD_STATE(
|
||||
env,
|
||||
iter->statement_reset_generation_ != iter->stmt_->reset_generation_,
|
||||
"iterator was invalidated");
|
||||
|
||||
// sqlite3_reset() can run JavaScript through an aggregate's xFinal, so it
|
||||
// stays inside the guard.
|
||||
SteppingStatementGuard stepping(iter->stmt_->db_.get(),
|
||||
iter->stmt_->statement_.get());
|
||||
int r = sqlite3_step(iter->stmt_->statement_.get());
|
||||
if (r != SQLITE_ROW) {
|
||||
CHECK_ERROR_OR_THROW(
|
||||
@@ -4000,11 +4088,18 @@ void StatementSyncIterator::Return(const FunctionCallbackInfo<Value>& args) {
|
||||
env, iter->stmt_->IsFinalized(), "statement has been finalized");
|
||||
Isolate* isolate = env->isolate();
|
||||
|
||||
// Unlike Next(), the reset result is intentionally ignored here: Return()
|
||||
// is invoked by the language during abrupt completion (e.g. a `throw`
|
||||
// inside a `for...of` body), and throwing on a deferred SQLite error
|
||||
// would discard the caller's already-pending exception.
|
||||
sqlite3_reset(iter->stmt_->statement_.get());
|
||||
if (!iter->done_) {
|
||||
// A language-invoked return() cannot be reached mid-step, since the loop
|
||||
// body only runs after next() has returned, so these guards only reject an
|
||||
// explicit call from inside a callback.
|
||||
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, iter->stmt_->db_.get());
|
||||
THROW_AND_RETURN_IF_STEPPING(env, iter->stmt_.get());
|
||||
// Unlike Next(), the reset result is intentionally ignored here: Return()
|
||||
// is invoked by the language during abrupt completion (e.g. a `throw`
|
||||
// inside a `for...of` body), and throwing on a deferred SQLite error
|
||||
// would discard the caller's already-pending exception.
|
||||
sqlite3_reset(iter->stmt_->statement_.get());
|
||||
}
|
||||
iter->done_ = true;
|
||||
|
||||
auto iter_template = getLazyIterTemplate(env);
|
||||
@@ -4080,6 +4175,7 @@ void Session::Changeset(const FunctionCallbackInfo<Value>& args) {
|
||||
env, !session->database_->IsOpen(), "database is not open");
|
||||
THROW_AND_RETURN_ON_BAD_STATE(
|
||||
env, session->session_ == nullptr, "session is not open");
|
||||
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, session->database_.get());
|
||||
|
||||
int nChangeset;
|
||||
void* pChangeset;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "sqlite3.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <list>
|
||||
#include <map>
|
||||
@@ -265,6 +266,26 @@ class DatabaseSync : public BaseObject {
|
||||
void DecrementCallbackDepth() { --callback_depth_; }
|
||||
bool IsInCallback() const { return callback_depth_ > 0; }
|
||||
|
||||
// SQLite forbids an authorizer callback from doing anything that modifies
|
||||
// the database connection that invoked it, which includes preparing and
|
||||
// stepping statements. See https://www.sqlite.org/c3ref/set_authorizer.html.
|
||||
void IncrementAuthorizerDepth() { ++authorizer_depth_; }
|
||||
void DecrementAuthorizerDepth() { --authorizer_depth_; }
|
||||
bool IsInAuthorizerCallback() const { return authorizer_depth_ > 0; }
|
||||
|
||||
// Finalizing a statement frees its virtual machine, so a callback that
|
||||
// SQLite invokes from inside sqlite3_step() must not finalize the statement
|
||||
// being stepped. Other statements on the connection are safe to finalize.
|
||||
void PushSteppingStatement(sqlite3_stmt* stmt) {
|
||||
stepping_statements_.push_back(stmt);
|
||||
}
|
||||
void PopSteppingStatement() { stepping_statements_.pop_back(); }
|
||||
bool IsSteppingStatement(sqlite3_stmt* stmt) const {
|
||||
return std::find(stepping_statements_.begin(),
|
||||
stepping_statements_.end(),
|
||||
stmt) != stepping_statements_.end();
|
||||
}
|
||||
|
||||
SET_MEMORY_INFO_NAME(DatabaseSync)
|
||||
SET_SELF_SIZE(DatabaseSync)
|
||||
|
||||
@@ -279,6 +300,8 @@ class DatabaseSync : public BaseObject {
|
||||
sqlite3* connection_;
|
||||
bool ignore_next_sqlite_error_;
|
||||
int callback_depth_ = 0;
|
||||
int authorizer_depth_ = 0;
|
||||
std::vector<sqlite3_stmt*> stepping_statements_;
|
||||
|
||||
std::set<BackupJob*> backups_;
|
||||
std::unordered_set<Session*> sessions_;
|
||||
@@ -460,6 +483,32 @@ class CallbackDepthGuard {
|
||||
DatabaseSync* db_;
|
||||
};
|
||||
|
||||
class SteppingStatementGuard {
|
||||
public:
|
||||
SteppingStatementGuard(DatabaseSync* db, sqlite3_stmt* stmt) : db_(db) {
|
||||
db_->PushSteppingStatement(stmt);
|
||||
}
|
||||
~SteppingStatementGuard() { db_->PopSteppingStatement(); }
|
||||
SteppingStatementGuard(const SteppingStatementGuard&) = delete;
|
||||
SteppingStatementGuard& operator=(const SteppingStatementGuard&) = delete;
|
||||
|
||||
private:
|
||||
DatabaseSync* db_;
|
||||
};
|
||||
|
||||
class AuthorizerDepthGuard {
|
||||
public:
|
||||
explicit AuthorizerDepthGuard(DatabaseSync* db) : db_(db) {
|
||||
db_->IncrementAuthorizerDepth();
|
||||
}
|
||||
~AuthorizerDepthGuard() { db_->DecrementAuthorizerDepth(); }
|
||||
AuthorizerDepthGuard(const AuthorizerDepthGuard&) = delete;
|
||||
AuthorizerDepthGuard& operator=(const AuthorizerDepthGuard&) = delete;
|
||||
|
||||
private:
|
||||
DatabaseSync* db_;
|
||||
};
|
||||
|
||||
class UserDefinedFunction {
|
||||
public:
|
||||
UserDefinedFunction(Environment* env,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const { skipIfSQLiteMissing } = require('../common');
|
||||
skipIfSQLiteMissing();
|
||||
const common = require('../common');
|
||||
common.skipIfSQLiteMissing();
|
||||
|
||||
const assert = require('node:assert');
|
||||
const { DatabaseSync, constants } = require('node:sqlite');
|
||||
@@ -288,3 +288,329 @@ suite('DatabaseSync.prototype.setAuthorizer()', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// SQLite forbids an authorizer callback from modifying the connection that
|
||||
// invoked it, which includes preparing and stepping statements.
|
||||
// See https://www.sqlite.org/c3ref/set_authorizer.html.
|
||||
suite('authorizer callback reentrancy', () => {
|
||||
const expectedError = 'ERR_INVALID_STATE: database cannot be accessed ' +
|
||||
'from an authorizer callback';
|
||||
const steppingError =
|
||||
'ERR_INVALID_STATE: statement is already being executed';
|
||||
|
||||
// Calls each of `cases` from inside an authorizer callback, and returns a
|
||||
// `name -> outcome` map of what each one threw.
|
||||
const runInAuthorizer = (db, cases) => {
|
||||
const outcomes = {};
|
||||
for (const [name, fn] of Object.entries(cases)) {
|
||||
let ran = false;
|
||||
db.setAuthorizer(() => {
|
||||
if (!ran) {
|
||||
ran = true;
|
||||
try {
|
||||
fn();
|
||||
outcomes[name] = 'did not throw';
|
||||
} catch (err) {
|
||||
outcomes[name] = `${err.code}: ${err.message}`;
|
||||
}
|
||||
}
|
||||
return constants.SQLITE_OK;
|
||||
});
|
||||
db.exec('SELECT 1');
|
||||
db.setAuthorizer(null);
|
||||
if (!ran) {
|
||||
outcomes[name] = 'authorizer callback did not run';
|
||||
}
|
||||
}
|
||||
return outcomes;
|
||||
};
|
||||
|
||||
// Builds the expected `name -> outcome` map for the given case names.
|
||||
const allRejected = (cases) => Object.fromEntries(
|
||||
Object.keys(cases).map((name) => [name, expectedError]),
|
||||
);
|
||||
|
||||
it('rejects database methods', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE t (x INTEGER)');
|
||||
const cases = {
|
||||
prepare: () => db.prepare('SELECT 1'),
|
||||
exec: () => db.exec('SELECT 1'),
|
||||
setAuthorizer: () => db.setAuthorizer(null),
|
||||
createSession: () => db.createSession(),
|
||||
applyChangeset: () => db.applyChangeset(new Uint8Array([1])),
|
||||
createTagStore: () => db.createTagStore(),
|
||||
serialize: () => db.serialize(),
|
||||
function: () => db.function('noop', () => 1),
|
||||
aggregate: () => db.aggregate('agg', { start: 0, step: (acc) => acc }),
|
||||
enableLoadExtension: () => db.enableLoadExtension(false),
|
||||
enableDefensive: () => db.enableDefensive(true),
|
||||
limits: () => { db.limits.length = 100; },
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases));
|
||||
});
|
||||
|
||||
// loadExtension() checks that extension loading is enabled before reaching
|
||||
// the authorizer guard, so it needs a database opened with allowExtension.
|
||||
it('rejects loadExtension', () => {
|
||||
const db = new DatabaseSync(':memory:', { allowExtension: true });
|
||||
db.enableLoadExtension(true);
|
||||
db.exec('CREATE TABLE t (x INTEGER)');
|
||||
const cases = {
|
||||
loadExtension: () => db.loadExtension('/nonexistent/extension'),
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases));
|
||||
});
|
||||
|
||||
// close() and deserialize() tear down the connection, so the pre-existing
|
||||
// callback depth guard already rejects them with its own message.
|
||||
it('rejects methods the callback depth guard already covers', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE t (x INTEGER)');
|
||||
const snapshot = db.serialize();
|
||||
const cases = {
|
||||
close: () => db.close(),
|
||||
deserialize: () => db.deserialize(snapshot),
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(runInAuthorizer(db, cases), {
|
||||
close: 'ERR_INVALID_STATE: database cannot be closed while in a callback',
|
||||
deserialize: 'ERR_INVALID_STATE: database cannot be deserialized ' +
|
||||
'while in a callback',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects statement methods', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE t (x INTEGER)');
|
||||
db.exec('INSERT INTO t VALUES (1)');
|
||||
const stmt = db.prepare('SELECT x FROM t');
|
||||
const cases = {
|
||||
run: () => stmt.run(),
|
||||
get: () => stmt.get(),
|
||||
all: () => stmt.all(),
|
||||
iterate: () => stmt.iterate(),
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases));
|
||||
});
|
||||
|
||||
// Only the statement being stepped is unsafe to finalize. Other statements
|
||||
// on the connection have their own virtual machines, so finalizing them from
|
||||
// a callback is allowed.
|
||||
it('allows finalizing a statement that is not being executed', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE t (x INTEGER)');
|
||||
db.exec('INSERT INTO t VALUES (1)');
|
||||
const closeStmt = db.prepare('SELECT x FROM t');
|
||||
const disposeStmt = db.prepare('SELECT x FROM t');
|
||||
const cases = {
|
||||
close: () => closeStmt.close(),
|
||||
dispose: () => disposeStmt[Symbol.dispose](),
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(runInAuthorizer(db, cases), {
|
||||
close: 'did not throw',
|
||||
dispose: 'did not throw',
|
||||
});
|
||||
});
|
||||
|
||||
// Disposal is idempotent, so a statement that is already finalized must stay
|
||||
// a no-op even inside a callback. Throwing here would turn a `using` scope's
|
||||
// real exception into a SuppressedError.
|
||||
it('allows disposing an already-finalized statement', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE t (x INTEGER)');
|
||||
const stmt = db.prepare('SELECT x FROM t');
|
||||
stmt.close();
|
||||
const cases = { dispose: () => stmt[Symbol.dispose]() };
|
||||
|
||||
assert.deepStrictEqual(runInAuthorizer(db, cases), {
|
||||
dispose: 'did not throw',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects session changeset methods', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE t (x INTEGER PRIMARY KEY, y TEXT)');
|
||||
const session = db.createSession({ table: 't' });
|
||||
db.exec("INSERT INTO t VALUES (1, 'a')");
|
||||
const cases = {
|
||||
changeset: () => session.changeset(),
|
||||
patchset: () => session.patchset(),
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases));
|
||||
});
|
||||
|
||||
// A statement being re-prepared inside sqlite3_step() is the case that
|
||||
// actually crashes, because that statement's VM is mid-execution.
|
||||
it('rejects finalizing the statement being stepped', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE t (x INTEGER)');
|
||||
db.exec('INSERT INTO t VALUES (1)');
|
||||
const stmt = db.prepare('SELECT x FROM t');
|
||||
stmt.get();
|
||||
db.exec('ALTER TABLE t ADD COLUMN y INTEGER');
|
||||
|
||||
let outcome = 'authorizer callback did not run';
|
||||
let ran = false;
|
||||
db.setAuthorizer(() => {
|
||||
if (!ran) {
|
||||
ran = true;
|
||||
try {
|
||||
stmt.close();
|
||||
outcome = 'did not throw';
|
||||
} catch (err) {
|
||||
outcome = `${err.code}: ${err.message}`;
|
||||
}
|
||||
}
|
||||
return constants.SQLITE_OK;
|
||||
});
|
||||
|
||||
stmt.get();
|
||||
|
||||
assert.strictEqual(outcome, steppingError);
|
||||
});
|
||||
|
||||
// Unlike an already-finalized statement, disposing the one being stepped
|
||||
// would free the running virtual machine, so it throws.
|
||||
it('rejects disposing the statement being stepped', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE t (x INTEGER)');
|
||||
db.exec('INSERT INTO t VALUES (1)');
|
||||
const stmt = db.prepare('SELECT x FROM t');
|
||||
stmt.get();
|
||||
db.exec('ALTER TABLE t ADD COLUMN y INTEGER');
|
||||
|
||||
let outcome = 'authorizer callback did not run';
|
||||
let ran = false;
|
||||
db.setAuthorizer(() => {
|
||||
if (!ran) {
|
||||
ran = true;
|
||||
try {
|
||||
stmt[Symbol.dispose]();
|
||||
outcome = 'did not throw';
|
||||
} catch (err) {
|
||||
outcome = `${err.code}: ${err.message}`;
|
||||
}
|
||||
}
|
||||
return constants.SQLITE_OK;
|
||||
});
|
||||
|
||||
stmt.get();
|
||||
|
||||
assert.strictEqual(outcome, steppingError);
|
||||
});
|
||||
|
||||
it('rejects iterator methods', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE t (x INTEGER)');
|
||||
db.exec('INSERT INTO t VALUES (1), (2)');
|
||||
const iter = db.prepare('SELECT x FROM t').iterate();
|
||||
const cases = {
|
||||
next: () => iter.next(),
|
||||
return: () => iter.return(),
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases));
|
||||
iter.return();
|
||||
});
|
||||
|
||||
// A drained iterator holds no SQLite state, so next() and return() stay
|
||||
// available and remain idempotent inside a callback.
|
||||
it('allows iterator methods on a drained iterator', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE t (x INTEGER)');
|
||||
db.exec('INSERT INTO t VALUES (1)');
|
||||
const iter = db.prepare('SELECT x FROM t').iterate();
|
||||
for (const row of iter) {
|
||||
assert.ok(row);
|
||||
}
|
||||
const done = {};
|
||||
const cases = {
|
||||
next: () => { done.next = iter.next().done; },
|
||||
return: () => { done.return = iter.return().done; },
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(runInAuthorizer(db, cases), {
|
||||
next: 'did not throw',
|
||||
return: 'did not throw',
|
||||
});
|
||||
assert.deepStrictEqual(done, { next: true, return: true });
|
||||
});
|
||||
|
||||
it('rejects tag store methods', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE t (x INTEGER)');
|
||||
const sql = db.createTagStore(10);
|
||||
const cases = {
|
||||
run: () => sql.run`SELECT 1`,
|
||||
get: () => sql.get`SELECT 1`,
|
||||
all: () => sql.all`SELECT 1`,
|
||||
iterate: () => sql.iterate`SELECT 1`,
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases));
|
||||
});
|
||||
|
||||
// clear() only drops cached statements, so invalidating the cache after a
|
||||
// schema change is allowed from the callback.
|
||||
it('allows clearing a tag store', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE t (x INTEGER)');
|
||||
db.exec('INSERT INTO t VALUES (1)');
|
||||
const sql = db.createTagStore(10);
|
||||
assert.strictEqual(sql.all`SELECT x FROM t`.length, 1);
|
||||
assert.strictEqual(sql.size, 1);
|
||||
const cases = { clear: () => sql.clear() };
|
||||
|
||||
assert.deepStrictEqual(runInAuthorizer(db, cases), {
|
||||
clear: 'did not throw',
|
||||
});
|
||||
assert.strictEqual(sql.size, 0);
|
||||
});
|
||||
|
||||
// A statement may be re-prepared during sqlite3_step() after a schema
|
||||
// change, which invokes the authorizer without an explicit prepare() call.
|
||||
it('rejects reentry when the authorizer runs during a re-prepare', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE t (x INTEGER)');
|
||||
db.exec('INSERT INTO t VALUES (1)');
|
||||
const stmt = db.prepare('SELECT x FROM t');
|
||||
stmt.get();
|
||||
db.exec('ALTER TABLE t ADD COLUMN y INTEGER');
|
||||
|
||||
let outcome = 'authorizer callback did not run';
|
||||
let ran = false;
|
||||
db.setAuthorizer(() => {
|
||||
if (!ran) {
|
||||
ran = true;
|
||||
try {
|
||||
db.prepare('SELECT 1');
|
||||
outcome = 'did not throw';
|
||||
} catch (err) {
|
||||
outcome = `${err.code}: ${err.message}`;
|
||||
}
|
||||
}
|
||||
return constants.SQLITE_OK;
|
||||
});
|
||||
|
||||
stmt.get();
|
||||
|
||||
assert.strictEqual(outcome, expectedError);
|
||||
});
|
||||
|
||||
it('allows access again after the authorizer returns', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE t (x INTEGER)');
|
||||
const cases = { prepare: () => db.prepare('SELECT 1') };
|
||||
|
||||
assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases));
|
||||
|
||||
db.setAuthorizer(() => constants.SQLITE_OK);
|
||||
assert.deepStrictEqual(db.prepare('SELECT 1 AS v').get(), { __proto__: null, v: 1 });
|
||||
});
|
||||
});
|
||||
@@ -36,4 +36,207 @@ for (const method of ['all', 'get', 'run', 'iterate']) {
|
||||
assert.strictEqual(db.isOpen, true);
|
||||
db.close();
|
||||
});
|
||||
|
||||
// Finalizing the statement being stepped frees the virtual machine that
|
||||
// sqlite3_step() is still running, so this must throw rather than crash.
|
||||
test(`statement.close() from a UDF during statement.${method}()`, () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec(`
|
||||
CREATE TABLE data (value INTEGER);
|
||||
INSERT INTO data VALUES (1), (2), (3);
|
||||
`);
|
||||
|
||||
let statement;
|
||||
db.function('close_stmt', (value) => {
|
||||
statement.close();
|
||||
return value;
|
||||
});
|
||||
|
||||
statement = db.prepare('SELECT close_stmt(value) FROM data');
|
||||
assert.throws(() => {
|
||||
if (method === 'iterate') {
|
||||
for (const row of statement.iterate()) {
|
||||
assert.ok(row);
|
||||
}
|
||||
} else {
|
||||
statement[method]();
|
||||
}
|
||||
}, {
|
||||
code: 'ERR_INVALID_STATE',
|
||||
message: 'statement is already being executed',
|
||||
});
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
// Re-running the statement being stepped resets its virtual machine
|
||||
// mid-execution, which is the same use-after-free as finalizing it.
|
||||
for (const reentrant of ['run', 'get', 'all', 'iterate']) {
|
||||
test(`statement.${reentrant}() from a UDF during ` +
|
||||
`statement.${method}()`, () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec(`
|
||||
CREATE TABLE data (value INTEGER, padding TEXT);
|
||||
INSERT INTO data VALUES (1, '${'x'.repeat(400)}'),
|
||||
(2, '${'y'.repeat(400)}'),
|
||||
(3, '${'z'.repeat(400)}');
|
||||
`);
|
||||
|
||||
let statement;
|
||||
let thrown;
|
||||
db.function('reenter', (value) => {
|
||||
if (thrown === undefined) {
|
||||
try {
|
||||
statement[reentrant]();
|
||||
thrown = null;
|
||||
} catch (err) {
|
||||
thrown = err;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
statement = db.prepare('SELECT reenter(value), padding FROM data');
|
||||
if (method === 'iterate') {
|
||||
for (const row of statement.iterate()) {
|
||||
assert.ok(row);
|
||||
}
|
||||
} else {
|
||||
statement[method]();
|
||||
}
|
||||
|
||||
assert.ok(thrown, `${reentrant}() was not rejected`);
|
||||
assert.strictEqual(thrown.code, 'ERR_INVALID_STATE');
|
||||
assert.strictEqual(thrown.message, 'statement is already being executed');
|
||||
|
||||
db.close();
|
||||
});
|
||||
}
|
||||
|
||||
// Tag store methods resolve to a cached statement, which may be the one
|
||||
// currently being stepped. Each reentrant method has its own guard, so all
|
||||
// four are exercised.
|
||||
for (const reentrant of ['run', 'get', 'all', 'iterate']) {
|
||||
test(`tag store ${reentrant} reentry during statement.${method}()`, () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
const sql = db.createTagStore(10);
|
||||
db.exec(`
|
||||
CREATE TABLE data (value INTEGER, padding TEXT);
|
||||
INSERT INTO data VALUES (1, '${'x'.repeat(400)}'),
|
||||
(2, '${'y'.repeat(400)}');
|
||||
`);
|
||||
|
||||
let thrown;
|
||||
db.function('reenter_tag', (value) => {
|
||||
if (thrown === undefined) {
|
||||
try {
|
||||
// The identical tagged literal resolves to the same cached
|
||||
// statement that is mid-execution.
|
||||
// All four reject at call time, iterate() included, so the
|
||||
// result is never consumed.
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
sql[reentrant]`SELECT reenter_tag(value), padding FROM data`;
|
||||
thrown = null;
|
||||
} catch (err) {
|
||||
thrown = err;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
if (method === 'iterate') {
|
||||
for (const row of sql.iterate`SELECT reenter_tag(value), padding FROM data`) {
|
||||
assert.ok(row);
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
sql[method]`SELECT reenter_tag(value), padding FROM data`;
|
||||
}
|
||||
|
||||
assert.ok(thrown, `tag store ${reentrant} reentry was not rejected`);
|
||||
assert.strictEqual(thrown.code, 'ERR_INVALID_STATE');
|
||||
assert.strictEqual(thrown.message,
|
||||
'statement is already being executed');
|
||||
|
||||
db.close();
|
||||
});
|
||||
}
|
||||
|
||||
// A UDF may prepare and finalize its own helper statements. Only the
|
||||
// statement being stepped is off limits.
|
||||
test(`UDF finalizes its own statement during statement.${method}()`, () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec(`
|
||||
CREATE TABLE data (value INTEGER);
|
||||
INSERT INTO data VALUES (1), (2), (3);
|
||||
CREATE TABLE lookup (key INTEGER, label TEXT);
|
||||
INSERT INTO lookup VALUES (1, 'one'), (2, 'two'), (3, 'three');
|
||||
`);
|
||||
|
||||
db.function('lookup_label', (value) => {
|
||||
const helper = db.prepare('SELECT label FROM lookup WHERE key = ?');
|
||||
const label = helper.get(value).label;
|
||||
helper.close();
|
||||
return label;
|
||||
});
|
||||
|
||||
const statement = db.prepare('SELECT lookup_label(value) AS l FROM data');
|
||||
if (method === 'iterate') {
|
||||
const labels = [];
|
||||
for (const row of statement.iterate()) {
|
||||
labels.push(row.l);
|
||||
}
|
||||
assert.deepStrictEqual(labels, ['one', 'two', 'three']);
|
||||
} else if (method === 'all') {
|
||||
assert.deepStrictEqual(statement.all().map((r) => r.l),
|
||||
['one', 'two', 'three']);
|
||||
} else if (method === 'get') {
|
||||
assert.strictEqual(statement.get().l, 'one');
|
||||
} else {
|
||||
statement.run();
|
||||
}
|
||||
|
||||
db.close();
|
||||
});
|
||||
}
|
||||
|
||||
// iterator.return() resets the statement it is iterating, and next() steps it
|
||||
// again, so both reach the virtual machine that is mid-execution.
|
||||
for (const op of ['next', 'return']) {
|
||||
test(`iterator.${op}() from a UDF during iteration`, () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec(`
|
||||
CREATE TABLE data (value INTEGER, padding TEXT);
|
||||
INSERT INTO data VALUES (1, '${'x'.repeat(400)}'),
|
||||
(2, '${'y'.repeat(400)}'),
|
||||
(3, '${'z'.repeat(400)}');
|
||||
`);
|
||||
|
||||
let iterator;
|
||||
let thrown;
|
||||
db.function('reenter_iter', (value) => {
|
||||
if (thrown === undefined && iterator !== undefined) {
|
||||
try {
|
||||
iterator[op]();
|
||||
thrown = null;
|
||||
} catch (err) {
|
||||
thrown = err;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
const statement = db.prepare(
|
||||
'SELECT reenter_iter(value) AS v, padding FROM data');
|
||||
iterator = statement.iterate();
|
||||
for (const row of iterator) {
|
||||
assert.ok(row);
|
||||
}
|
||||
|
||||
assert.ok(thrown, `iterator.${op}() was not rejected`);
|
||||
assert.strictEqual(thrown.code, 'ERR_INVALID_STATE');
|
||||
assert.strictEqual(thrown.message, 'statement is already being executed');
|
||||
|
||||
db.close();
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user