Skip to content

Commit e8ea485

Browse files
committed
sqlite: do not leave database open after failed open
When sqlite3_open_v2() fails, SQLite still assigns a database handle in a "sick" state. Such a handle may only be used to retrieve the error and must then be released with sqlite3_close(). Keeping it in connection_ meant that after a failed open(): - isOpen incorrectly reported true, - every method passed the "database is not open" check and called SQLite APIs on the sick handle, which is an API misuse, - registering a user-defined function leaked its user data in builds with SQLITE_ENABLE_API_ARMOR, because SQLite rejects the call before taking ownership of the user data, - open() could not be retried since the database appeared open. Close and reset the connection handle when open() fails at any point so that the database remains closed and open() can be retried. Fixes: #63831 Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
1 parent da00166 commit e8ea485

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

src/node_sqlite.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,19 @@ bool DatabaseSync::Open() {
941941
return false;
942942
}
943943

944+
// sqlite3_open_v2() assigns a database handle even when it fails. Such a
945+
// handle is in a "sick" state and may only be used to retrieve the error
946+
// and must then be released with sqlite3_close(). Close and reset the
947+
// handle on any failure below so that a failed open() does not leave the
948+
// database in an open state.
949+
bool opened = false;
950+
auto reset_connection_on_failure = OnScopeLeave([&]() {
951+
if (!opened && connection_ != nullptr) {
952+
sqlite3_close_v2(connection_);
953+
connection_ = nullptr;
954+
}
955+
});
956+
944957
// TODO(cjihrig): Support additional flags.
945958
int default_flags = SQLITE_OPEN_URI;
946959
int flags = open_config_.get_read_only()
@@ -1003,6 +1016,7 @@ bool DatabaseSync::Open() {
10031016
env()->isolate(), this, load_extension_ret, SQLITE_OK, false);
10041017
}
10051018

1019+
opened = true;
10061020
return true;
10071021
}
10081022

test/parallel/test-sqlite-database-sync.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
const { skipIfSQLiteMissing } = require('../common');
33
skipIfSQLiteMissing();
44
const tmpdir = require('../common/tmpdir');
5-
const { existsSync } = require('node:fs');
5+
const { existsSync, mkdirSync } = require('node:fs');
66
const { join } = require('node:path');
77
const { DatabaseSync, StatementSync } = require('node:sqlite');
88
const { suite, test } = require('node:test');
@@ -308,6 +308,42 @@ suite('DatabaseSync.prototype.open()', () => {
308308
});
309309
t.assert.strictEqual(db.isOpen, true);
310310
});
311+
312+
test('does not leave the database open after a failed open', (t) => {
313+
// Regression test for https://github.com/nodejs/node/issues/63831
314+
const dbDir = join(tmpdir.path, `database-dir-${cnt++}`);
315+
const dbPath = join(dbDir, 'failed-open.db');
316+
using db = new DatabaseSync(dbPath, { open: false });
317+
318+
// The directory does not exist, so opening the database fails.
319+
t.assert.throws(() => {
320+
db.open();
321+
}, {
322+
code: 'ERR_SQLITE_ERROR',
323+
message: /unable to open database file/,
324+
});
325+
t.assert.strictEqual(db.isOpen, false);
326+
327+
// The connection must not be usable after a failed open.
328+
t.assert.throws(() => {
329+
db.exec('SELECT 1');
330+
}, {
331+
code: 'ERR_INVALID_STATE',
332+
message: /database is not open/,
333+
});
334+
t.assert.throws(() => {
335+
db.function('fn', () => {});
336+
}, {
337+
code: 'ERR_INVALID_STATE',
338+
message: /database is not open/,
339+
});
340+
341+
// The database can be opened once the underlying problem is resolved.
342+
mkdirSync(dbDir);
343+
t.assert.strictEqual(db.open(), undefined);
344+
t.assert.strictEqual(db.isOpen, true);
345+
db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY)');
346+
});
311347
});
312348

313349
suite('DatabaseSync.prototype.close()', () => {

0 commit comments

Comments
 (0)