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
session.ts#L17-L25 calls HybridNitroSQLite.open() before openDatabaseQueue() checks whether that name is already open.
operations.cpp#L32-L52 keys dbMap only by dbName and unconditionally assigns dbMap[dbName] = db after opening.
- The previous pointer is overwritten without
sqlite3_close_v2(), so it is no longer reachable for closure.
- 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:
- Duplicate
open({ name, location }) leaves the original connection usable and unchanged.
- Same
name, different location never changes the first connection's PRAGMA database_list path.
- Direct native duplicate open cannot overwrite a registered handle.
- Failed duplicate open does not leak a connection (validated through lifecycle instrumentation or a test-only native handle counter).
Summary
Calling
open()twice with the same databasenamemutates 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 whenlocationdiffers.Verified on
mainatad8b835(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
session.ts#L17-L25callsHybridNitroSQLite.open()beforeopenDatabaseQueue()checks whether that name is already open.operations.cpp#L32-L52keysdbMaponly bydbNameand unconditionally assignsdbMap[dbName] = dbafter opening.sqlite3_close_v2(), so it is no longer reachable for closure.openDatabaseQueue()throwDatabase ... is already open.Because native execution also resolves a connection by
dbName, a failed second open with a differentlocationchanges which physical database the first returned JS connection uses.Smallest reproducer
With the same location, the overwrite still leaks the first
sqlite3*. Callingfirst.close()closes only the replacement handle.The direct
HybridNitroSQLite.open(name, location)API has the same unconditional overwrite and no native duplicate guard.Impact
open()call that reported failure.locationparticipates in the file path but not the map/queue key.Acceptance criteria
sqlite3_open_v2()or replacing any registry entry.open()fails.sqlite3*has exactly one reachable close path.Regression-test target
Harness tests for:
open({ name, location })leaves the original connection usable and unchanged.name, differentlocationnever changes the first connection'sPRAGMA database_listpath.