Version
main (verified on 2b350bb8e42)
Platform
Darwin 25.6.0 arm64 (macOS); not platform-specific
Subsystem
sqlite
What steps will reproduce the bug?
#64743 added a database-level callback-depth guard, so db.close() from inside a user-defined function now correctly throws ERR_INVALID_STATE instead of finalizing a statement mid-sqlite3_step() (that was #63180, now fixed). The guard is per-database, so it does not cover a callback reentering the specific statement that invoked it. SQLite forbids calling sqlite3_step() / sqlite3_reset() / sqlite3_finalize() on a statement while that statement's own callback is on the stack.
Two cases:
1. Reentrant iter.next() silently advances the iterator being consumed.
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)');
db.prepare('INSERT INTO t VALUES (1, 10)').run();
let iter;
db.function('reenter', () => {
iter.next(); // reenters the statement currently being stepped
return 0;
});
iter = db.prepare('SELECT reenter() FROM t').iterate();
console.log(iter.next());
2. Recursive stmt.get() on the running statement.
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync(':memory:');
let stmt;
db.function('x', () => stmt.get());
stmt = db.prepare('SELECT x()');
stmt.get();
How often does it reproduce? Is there a required condition?
Always. The callback must reenter the same statement object that is currently executing.
What is the expected behavior? Why is that the expected behavior?
Both should throw ERR_INVALID_STATE, consistent with how #64743 handles db.close() from a callback. Cross-statement use from a callback (the common "lookup" pattern — preparing or stepping a different statement) should keep working; only reentry into the running statement is forbidden by the C API.
What do you see instead?
Case 1: no error. The reentrant next() consumes rows from the same VM, so the outer iteration silently skips them:
reentrant next() SUCCEEDED
reentrant next() SUCCEEDED
first: {"done":false,"value":{"reenter()":0}}
Case 2: Maximum call stack size exceeded — a V8 stack overflow rather than a SQLite-level error, so the actual constraint is never reported.
Neither crashes, so this is a correctness issue rather than memory safety.
Additional information
The closed #63183 contained a per-statement IsStepping() RAII flag (MarkStepping() around every sqlite3_step caller, with JS-callable step/reset/finalize checking it) that addresses exactly this, plus regression tests for both cases. That PR was closed in favor of #64743, which covers only the database-level close() path, so the per-statement piece is currently unowned. It may be worth salvaging from that branch.
Version
main(verified on2b350bb8e42)Platform
Subsystem
sqlite
What steps will reproduce the bug?
#64743 added a database-level callback-depth guard, so
db.close()from inside a user-defined function now correctly throwsERR_INVALID_STATEinstead of finalizing a statement mid-sqlite3_step()(that was #63180, now fixed). The guard is per-database, so it does not cover a callback reentering the specific statement that invoked it. SQLite forbids callingsqlite3_step()/sqlite3_reset()/sqlite3_finalize()on a statement while that statement's own callback is on the stack.Two cases:
1. Reentrant
iter.next()silently advances the iterator being consumed.2. Recursive
stmt.get()on the running statement.How often does it reproduce? Is there a required condition?
Always. The callback must reenter the same statement object that is currently executing.
What is the expected behavior? Why is that the expected behavior?
Both should throw
ERR_INVALID_STATE, consistent with how #64743 handlesdb.close()from a callback. Cross-statement use from a callback (the common "lookup" pattern — preparing or stepping a different statement) should keep working; only reentry into the running statement is forbidden by the C API.What do you see instead?
Case 1: no error. The reentrant
next()consumes rows from the same VM, so the outer iteration silently skips them:Case 2:
Maximum call stack size exceeded— a V8 stack overflow rather than a SQLite-level error, so the actual constraint is never reported.Neither crashes, so this is a correctness issue rather than memory safety.
Additional information
The closed #63183 contained a per-statement
IsStepping()RAII flag (MarkStepping()around everysqlite3_stepcaller, with JS-callable step/reset/finalize checking it) that addresses exactly this, plus regression tests for both cases. That PR was closed in favor of #64743, which covers only the database-levelclose()path, so the per-statement piece is currently unowned. It may be worth salvaging from that branch.