Skip to content

Opening the same database name twice overwrites the native connection before validation #306

Description

@chrispader

Summary

Calling open() twice with the same database name mutates the native connection registry before the JavaScript duplicate-open check runs. The second native handle overwrites the first handle, the first handle leaks, and the original JS connection can start executing against a different database file when location differs.

Verified on main at ad8b835 (v9.7.0).

Expected behavior

Opening an already-open connection identity should fail atomically without changing the existing connection. A filename used in two different locations must not make one connection silently route to the other file.

Observed control flow

  1. session.ts#L17-L25 calls HybridNitroSQLite.open() before openDatabaseQueue() checks whether that name is already open.
  2. operations.cpp#L32-L52 keys dbMap only by dbName and unconditionally assigns dbMap[dbName] = db after opening.
  3. The previous pointer is overwritten without sqlite3_close_v2(), so it is no longer reachable for closure.
  4. Only then does openDatabaseQueue() throw Database ... is already open.

Because native execution also resolves a connection by dbName, a failed second open with a different location changes which physical database the first returned JS connection uses.

Smallest reproducer

const first = open({ name: 'spaces.sqlite', location: 'first' })
first.execute('CREATE TABLE first_marker (id INTEGER PRIMARY KEY)')

expect(() =>
  open({ name: 'spaces.sqlite', location: 'second' }),
).toThrow(/already open/)

// Expected: still points at first/spaces.sqlite and lists first_marker.
// Current control flow: native dbMap now points at second/spaces.sqlite.
const databaseList = first.execute('PRAGMA database_list')
const schema = first.execute(
  "SELECT name FROM sqlite_master WHERE type = 'table'",
)

With the same location, the overwrite still leaks the first sqlite3*. Calling first.close() closes only the replacement handle.

The direct HybridNitroSQLite.open(name, location) API has the same unconditional overwrite and no native duplicate guard.

Impact

  • Native connection leak on duplicate open.
  • A connection can silently switch to a different physical database after an open() call that reported failure.
  • Registry and queue identities disagree because location participates in the file path but not the map/queue key.
  • Closing the original JS connection can close the replacement database while leaving the original native handle open.

Acceptance criteria

  • Define a single connection identity that includes the resolved/normalized database path, or use an opaque connection ID consistently across JS and native layers.
  • Reject an invalid duplicate before calling sqlite3_open_v2() or replacing any registry entry.
  • Preserve the original connection unchanged when a subsequent open() fails.
  • Ensure every successfully created sqlite3* has exactly one reachable close path.
  • Support or explicitly reject simultaneous databases with the same filename in different locations without cross-routing.

Regression-test target

Harness tests for:

  1. Duplicate open({ name, location }) leaves the original connection usable and unchanged.
  2. Same name, different location never changes the first connection's PRAGMA database_list path.
  3. Direct native duplicate open cannot overwrite a registered handle.
  4. Failed duplicate open does not leak a connection (validated through lifecycle instrumentation or a test-only native handle counter).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions