Skip to content

sqlite: session[Symbol.dispose]() silently ignores the in-use guard #65447

Description

@arimxyer

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]().

Metadata

Metadata

Assignees

No one assigned

    Labels

    sqliteIssues and PRs related to the SQLite subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions