mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05: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
+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;
|
||||
|
||||
Reference in new issue
Block a user