diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 1b7c11cb69e..95cbe9f99f0 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -3266,6 +3266,7 @@ void StatementSync::All(const FunctionCallbackInfo& args) { THROW_AND_RETURN_IF_IN_AUTHORIZER(env, stmt->db_.get()); THROW_AND_RETURN_IF_STEPPING(env, stmt); Isolate* isolate = env->isolate(); + SteppingStatementGuard stepping(stmt->db_.get(), stmt->statement_.get()); int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_OK, void()); @@ -3298,6 +3299,7 @@ void StatementSync::Iterate(const FunctionCallbackInfo& args) { env, stmt->IsFinalized(), "statement has been finalized"); THROW_AND_RETURN_IF_IN_AUTHORIZER(env, stmt->db_.get()); THROW_AND_RETURN_IF_STEPPING(env, stmt); + SteppingStatementGuard stepping(stmt->db_.get(), stmt->statement_.get()); int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void()); @@ -3323,6 +3325,7 @@ void StatementSync::Get(const FunctionCallbackInfo& args) { env, stmt->IsFinalized(), "statement has been finalized"); THROW_AND_RETURN_IF_IN_AUTHORIZER(env, stmt->db_.get()); THROW_AND_RETURN_IF_STEPPING(env, stmt); + SteppingStatementGuard stepping(stmt->db_.get(), stmt->statement_.get()); int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void()); @@ -3349,6 +3352,7 @@ void StatementSync::Run(const FunctionCallbackInfo& args) { env, stmt->IsFinalized(), "statement has been finalized"); THROW_AND_RETURN_IF_IN_AUTHORIZER(env, stmt->db_.get()); THROW_AND_RETURN_IF_STEPPING(env, stmt); + SteppingStatementGuard stepping(stmt->db_.get(), stmt->statement_.get()); int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void()); diff --git a/test/parallel/test-sqlite-statement-binding-reentry.js b/test/parallel/test-sqlite-statement-binding-reentry.js new file mode 100644 index 00000000000..052803e72a8 --- /dev/null +++ b/test/parallel/test-sqlite-statement-binding-reentry.js @@ -0,0 +1,63 @@ +'use strict'; + +const { skipIfSQLiteMissing, mustCall } = require('../common'); +skipIfSQLiteMissing(); +const assert = require('node:assert'); +const { test } = require('node:test'); +const { DatabaseSync } = require('node:sqlite'); + +const reentryError = { + code: 'ERR_INVALID_STATE', + message: 'statement is already being executed', +}; + +// Binding a named parameter reads properties off the supplied object, so a +// getter runs JavaScript after the statement has been reset but before it is +// stepped. Reentering the same statement there resets it a second time and +// hands out a second iterator over one virtual machine. +for (const method of ['all', 'get', 'run', 'iterate']) { + test(`${method}() reentry during parameter binding is rejected`, () => { + const db = new DatabaseSync(':memory:'); + db.exec(` + CREATE TABLE data (value INTEGER); + INSERT INTO data VALUES (1), (2), (3); + `); + + let statement; + const invoke = (params) => (method === 'iterate' ? + [...statement.iterate(params)] : + statement[method](params)); + const reenter = mustCall(() => { + assert.throws(() => invoke({ $min: 2 }), reentryError); + return 1; + }); + const params = { get $min() { return reenter(); } }; + + statement = db.prepare('SELECT value FROM data WHERE value >= $min'); + invoke(params); + }); +} + +test('two iterators cannot share one virtual machine', () => { + const db = new DatabaseSync(':memory:'); + db.exec(` + CREATE TABLE data (value INTEGER); + INSERT INTO data VALUES (1), (2), (3); + `); + + const statement = db.prepare('SELECT value FROM data WHERE value >= $min'); + let inner; + const reenter = mustCall(() => { + assert.throws(() => { + inner = statement.iterate({ $min: 1 }); + }, reentryError); + return 1; + }); + const params = { get $min() { return reenter(); } }; + + assert.deepStrictEqual( + [...statement.iterate(params)].map((row) => row.value), + [1, 2, 3], + ); + assert.strictEqual(inner, undefined); +});