From 353609207154e0a9087d573981e9cde1325f2e57 Mon Sep 17 00:00:00 2001 From: Trevor Burnham Date: Sat, 8 Aug 2026 19:56:16 -0400 Subject: [PATCH] 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 an authorizer 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 the callback is on the stack. The depth is per-connection, so other connections stay usable from the callback. The guard covers every authorizer invocation, not just those from an explicit prepare(), since SQLite may re-prepare a statement during sqlite3_step() after a schema change. serialize() and the session changeset() and patchset() methods prepare statements internally, so they re-enter the authorizer too. Reentry through changeset() does not terminate: it recurses until the process is killed, with no way to catch it from JavaScript. Finalizing a statement is a separate hazard. It frees the virtual machine that the enclosing sqlite3_step() is still executing, which crashes rather than throwing, and any callback SQLite invokes during execution can reach it. statement.close() therefore rejects while any callback is on the stack, not just an authorizer, so the equivalent crash through a user-defined function is fixed as well. Disposal stays idempotent, since throwing for an already-finalized statement would demote a `using` scope's exception to a SuppressedError. Signed-off-by: Trevor Burnham Fixes: https://github.com/nodejs/node/issues/63207 Assisted-by: claude:opus-5 --- doc/api/sqlite.md | 20 +++ src/node_sqlite.cc | 61 +++++++ src/node_sqlite.h | 21 +++ test/parallel/test-sqlite-authz.js | 239 ++++++++++++++++++++++++- test/parallel/test-sqlite-udf-close.js | 32 ++++ 5 files changed, 371 insertions(+), 2 deletions(-) diff --git a/doc/api/sqlite.md b/doc/api/sqlite.md index 57a0007baabc..8a4c03fa09b0 100644 --- a/doc/api/sqlite.md +++ b/doc/api/sqlite.md @@ -439,6 +439,11 @@ wrapper around [`sqlite3_create_function_v2()`][]. * `callback` {Function|null} The authorizer function to set, or `null` to @@ -464,6 +469,21 @@ 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, `statement.close()` throws if called from any callback SQLite +invokes during execution, such as a user-defined function, because finalizing a +statement that is mid-execution would free the virtual machine that is running. + ```cjs const { DatabaseSync, constants } = require('node:sqlite'); const db = new DatabaseSync(':memory:'); diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 027372e42daa..9948ad6fcae3 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -96,6 +96,24 @@ inline MaybeLocal 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") + +// Finalizing a statement frees its virtual machine. Any callback that SQLite +// invokes from inside sqlite3_step() may be running on that very statement, so +// finalizing from one is a use-after-free rather than a contract violation. +#define THROW_AND_RETURN_IF_IN_CALLBACK(env, db) \ + THROW_AND_RETURN_ON_BAD_STATE( \ + (env), \ + (db)->IsInCallback(), \ + "statement cannot be finalized from a callback") + #define SQLITE_VALUE_TO_JS(from, isolate, use_big_int_args, result, ...) \ do { \ switch (sqlite3_##from##_type(__VA_ARGS__)) { \ @@ -825,6 +843,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."); @@ -1081,6 +1105,7 @@ void DatabaseSync::CreateTagStore(const FunctionCallbackInfo& 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()) { @@ -1483,6 +1508,7 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo& 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(), @@ -1606,6 +1632,7 @@ void DatabaseSync::Exec(const FunctionCallbackInfo& 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(), @@ -1630,6 +1657,7 @@ void DatabaseSync::CustomFunction(const FunctionCallbackInfo& 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(), @@ -1803,6 +1831,7 @@ void DatabaseSync::Serialize(const FunctionCallbackInfo& 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()) { @@ -1858,6 +1887,7 @@ void DatabaseSync::Deserialize(const FunctionCallbackInfo& 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(env->isolate(), @@ -1933,6 +1963,7 @@ void DatabaseSync::AggregateFunction(const FunctionCallbackInfo& 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()); Local options = args[1].As(); Local start_v; @@ -2144,6 +2175,7 @@ void DatabaseSync::CreateSession(const FunctionCallbackInfo& 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); @@ -2313,6 +2345,7 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo& 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( @@ -2447,6 +2480,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()) { @@ -2475,6 +2509,7 @@ void DatabaseSync::EnableDefensive(const FunctionCallbackInfo& 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()) { @@ -2500,6 +2535,7 @@ void DatabaseSync::LoadExtension(const FunctionCallbackInfo& 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(), @@ -2528,6 +2564,7 @@ void DatabaseSync::SetAuthorizer(const FunctionCallbackInfo& 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(); @@ -2564,6 +2601,7 @@ int DatabaseSync::AuthorizerCallback(void* user_data, const char* param4) { DatabaseSync* db = static_cast(user_data); CallbackDepthGuard guard(db); + AuthorizerDepthGuard authorizer_guard(db); Environment* env = db->env(); Isolate* isolate = env->isolate(); HandleScope handle_scope(isolate); @@ -2677,12 +2715,20 @@ void StatementSync::Close(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( env, stmt->IsFinalized(), "statement has been finalized"); + THROW_AND_RETURN_IF_IN_CALLBACK(env, stmt->db_.get()); stmt->Close(); } void StatementSync::Dispose(const FunctionCallbackInfo& 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_IN_CALLBACK(env, stmt->db_.get()); stmt->Close(); } @@ -3127,6 +3173,7 @@ void StatementSync::All(const FunctionCallbackInfo& 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()); Isolate* isolate = env->isolate(); int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_OK, void()); @@ -3154,6 +3201,7 @@ void StatementSync::Iterate(const FunctionCallbackInfo& 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()); int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void()); @@ -3177,6 +3225,7 @@ void StatementSync::Get(const FunctionCallbackInfo& 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()); int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void()); @@ -3201,6 +3250,7 @@ void StatementSync::Run(const FunctionCallbackInfo& 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()); int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void()); @@ -3483,6 +3533,7 @@ void SQLTagStore::Run(const FunctionCallbackInfo& 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 stmt = PrepareStatement(args); @@ -3509,6 +3560,7 @@ void SQLTagStore::Iterate(const FunctionCallbackInfo& 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 stmt = PrepareStatement(args); @@ -3537,6 +3589,7 @@ void SQLTagStore::Get(const FunctionCallbackInfo& 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 stmt = PrepareStatement(args); @@ -3566,6 +3619,7 @@ void SQLTagStore::All(const FunctionCallbackInfo& 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 stmt = PrepareStatement(args); @@ -3592,6 +3646,10 @@ void SQLTagStore::All(const FunctionCallbackInfo& args) { void SQLTagStore::Clear(const FunctionCallbackInfo& args) { SQLTagStore* store; ASSIGN_OR_RETURN_UNWRAP(&store, args.This()); + Environment* env = Environment::GetCurrent(args); + if (store->database_) { + THROW_AND_RETURN_IF_IN_AUTHORIZER(env, store->database_.get()); + } store->sql_tags_.Clear(); } @@ -3785,6 +3843,7 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( env, iter->stmt_->IsFinalized(), "statement has been finalized"); + THROW_AND_RETURN_IF_IN_AUTHORIZER(env, iter->stmt_->db_.get()); Isolate* isolate = env->isolate(); auto iter_template = getLazyIterTemplate(env); @@ -3862,6 +3921,7 @@ void StatementSyncIterator::Return(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( env, iter->stmt_->IsFinalized(), "statement has been finalized"); + THROW_AND_RETURN_IF_IN_AUTHORIZER(env, iter->stmt_->db_.get()); Isolate* isolate = env->isolate(); sqlite3_reset(iter->stmt_->statement_); @@ -3940,6 +4000,7 @@ void Session::Changeset(const FunctionCallbackInfo& 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; diff --git a/src/node_sqlite.h b/src/node_sqlite.h index b4446e5db859..1d0117dd3b72 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -233,6 +233,13 @@ 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; } + SET_MEMORY_INFO_NAME(DatabaseSync) SET_SELF_SIZE(DatabaseSync) @@ -247,6 +254,7 @@ class DatabaseSync : public BaseObject { sqlite3* connection_; bool ignore_next_sqlite_error_; int callback_depth_ = 0; + int authorizer_depth_ = 0; std::set backups_; std::unordered_set sessions_; @@ -426,6 +434,19 @@ class CallbackDepthGuard { 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, diff --git a/test/parallel/test-sqlite-authz.js b/test/parallel/test-sqlite-authz.js index 69c075a57e2e..a02035d5fe1f 100644 --- a/test/parallel/test-sqlite-authz.js +++ b/test/parallel/test-sqlite-authz.js @@ -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,238 @@ 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 finalizeError = 'ERR_INVALID_STATE: statement cannot be finalized ' + + 'from a callback'; + + // 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), + deserialize: () => db.deserialize(db.serialize()), + 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), + limits: () => { db.limits.length = 100; }, + }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases)); + }); + + it('rejects close(), which the callback depth guard already covers', () => { + const db = new DatabaseSync(':memory:'); + const cases = { close: () => db.close() }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), { + close: 'ERR_INVALID_STATE: database cannot be closed 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)); + }); + + // Finalizing a statement whose sqlite3_step() frame is still on the stack + // frees the VM that step is executing, so these must be rejected rather + // than crashing. + it('rejects finalizing a statement', () => { + 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: finalizeError, + dispose: finalizeError, + }); + }); + + // 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, finalizeError); + }); + + it('rejects iterator methods', () => { + 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(); + const cases = { + next: () => iter.next(), + return: () => iter.return(), + }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases)); + iter.return(); + }); + + 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`, + clear: () => sql.clear(), + }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases)); + }); + + // 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 }); + }); +}); diff --git a/test/parallel/test-sqlite-udf-close.js b/test/parallel/test-sqlite-udf-close.js index 86794029b457..16c49cae8a41 100644 --- a/test/parallel/test-sqlite-udf-close.js +++ b/test/parallel/test-sqlite-udf-close.js @@ -36,4 +36,36 @@ 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 cannot be finalized from a callback', + }); + + db.close(); + }); }