diff --git a/doc/api/sqlite.md b/doc/api/sqlite.md index 57a0007baab..8a4c03fa09b 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 027372e42da..9948ad6fcae 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 b4446e5db85..1d0117dd3b7 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 69c075a57e2..a02035d5fe1 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 86794029b45..16c49cae8a4 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(); + }); }