Version
v27.0.0-pre (current main)
Platform
Linux ari-cachyos 7.1.8-1-cachyos #1 SMP PREEMPT_DYNAMIC Mon, 10 Aug 2026 19:39:40 +0000 x86_64 GNU/Linux
Subsystem
node:sqlite
What steps will reproduce the bug?
const { DatabaseSync, constants } = require('node:sqlite');
for (const [name, close] of [
['close()', (session) => session.close()],
['[Symbol.dispose]()', (session) => session[Symbol.dispose]()],
]) {
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)');
const session = db.createSession({ table: 'data' });
db.exec("INSERT INTO data VALUES (1, 'value')");
let attempted = false;
db.setAuthorizer(() => {
if (!attempted) {
attempted = true;
try {
close(session);
console.log(`${name}: no throw`);
} catch (error) {
console.log(`${name}: ${error.code}: ${error.message}`);
}
}
return constants.SQLITE_OK;
});
session.changeset();
db.setAuthorizer(null);
session.close();
db.close();
}
How often does it reproduce? Is there a required condition?
Every time disposal is attempted while the session is generating a changeset or patchset.
What is the expected behavior? Why is that the expected behavior?
session[Symbol.dispose]() should throw the same ERR_INVALID_STATE as session.close() when the session cannot be closed safely. StatementSync already follows this pattern: its disposal propagates the active-statement guard, with coverage in test/parallel/test-sqlite-diagnostic-channel.js under “subscriber cannot close the database or statement”.
Expected output:
close(): ERR_INVALID_STATE: session is currently in use
[Symbol.dispose](): ERR_INVALID_STATE: session is currently in use
What do you see instead?
close(): ERR_INVALID_STATE: session is currently in use
[Symbol.dispose](): no throw
Session::Dispose() catches and discards the exception from Session::Close(). The session remains open even though disposal appeared to succeed.
Additional information
The reentrancy guard added by #65349 works: sqlite3session_delete() is not called while changeset generation is active, so this does not reproduce the use-after-free that the guard prevents. The remaining bug is the silent no-op and inconsistent error propagation from [Symbol.dispose]().
Version
v27.0.0-pre (current main)
Platform
Subsystem
node:sqlite
What steps will reproduce the bug?
How often does it reproduce? Is there a required condition?
Every time disposal is attempted while the session is generating a changeset or patchset.
What is the expected behavior? Why is that the expected behavior?
session[Symbol.dispose]()should throw the sameERR_INVALID_STATEassession.close()when the session cannot be closed safely.StatementSyncalready follows this pattern: its disposal propagates the active-statement guard, with coverage intest/parallel/test-sqlite-diagnostic-channel.jsunder “subscriber cannot close the database or statement”.Expected output:
What do you see instead?
Session::Dispose()catches and discards the exception fromSession::Close(). The session remains open even though disposal appeared to succeed.Additional information
The reentrancy guard added by #65349 works:
sqlite3session_delete()is not called while changeset generation is active, so this does not reproduce the use-after-free that the guard prevents. The remaining bug is the silent no-op and inconsistent error propagation from[Symbol.dispose]().