Repository navigation
node:sqlite segfaults when db.close() is called from a user-defined function callback during query execution #63180
Description
Version
all current versions (tested v22.22.2, v24.15.0, v25.9.0, and v26.1.0)
Platform
All platforms are impacted.
(but because you asked:)
$ uname -a
Linux swift 6.17.0-23-generic #23~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 14 16:11:48 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=24.04
DISTRIB_CODENAME=noble
DISTRIB_DESCRIPTION="Ubuntu 24.04.4 LTS"
Subsystem
sqlite
What steps will reproduce the bug?
There are two distinct crash classes, both triggered by db.close() from inside a user-defined function callback. The original .all() repro and the .run() repro below are verified to segfault on main; the other Class A variants are expected to segfault by code inspection — they all funnel through the same StatementExecutionHelper step sites.
Class A — Statement VM freed mid-step. The original report; affects every StatementSync execution path that runs sqlite3_step:
// .all() — segfaults
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync(":memory:");
db.exec("CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)");
db.prepare("INSERT INTO t VALUES (1, 10)").run();
db.prepare("INSERT INTO t VALUES (2, 20)").run();
db.function("close_db", (x) => { try { db.close(); } catch {} return x; });
db.prepare("SELECT close_db(v) FROM t").all();// .get() — same crash
db.prepare("SELECT close_db(v) FROM t").get();// iterator — same crash
const iter = db.prepare("SELECT close_db(v) FROM t").iterate();
iter.next();// SQL tag store .all() / .get() — same crash
const sql = db.createTagStore(4);
sql.all`SELECT close_db(v) FROM t`;Class B — db->Connection() null deref after step. Distinct crash path, only surfaces in .run() (and the SQL-tag .run()), even after Class A is fixed by deferring finalize. After sqlite3_step returns, StatementExecutionHelper::Run reads sqlite3_last_insert_rowid(db->Connection()) and sqlite3_changes64(db->Connection()). The reentrant db.close() set db->connection_ = nullptr before step returned, and the bundled SQLite is built without SQLITE_ENABLE_API_ARMOR, so passing NULL to those calls is a null-pointer deref:
// .run() — segfaults via the connection-null path
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync(":memory:");
db.exec("CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)");
db.function("close_db", (x) => { try { db.close(); } catch {} return x; });
db.prepare("INSERT INTO t SELECT close_db(1), close_db(2)").run();// SQL tag store .run() — same crash
const sql = db.createTagStore(4);
sql.run`INSERT INTO t SELECT close_db(${1}), close_db(${2})`;How often does it reproduce? Is there a required condition?
100%.
The user-function callback must call db.close() while the outer sqlite3_step is still on the call stack.
What is the expected behavior? Why is that the expected behavior?
Either a clean error ("database closed during query" / SQLITE_INTERRUPT-style) or graceful completion of the in-flight statement followed by close. Process must not crash.
What do you see instead?
Segmentation fault (core dumped), exit code 139 on linux
Additional information
(full disclosure: produced with assistance from claude)
Activity
TrevorBurnham commentedon Aug 7, 2026
I believe this was fixed by #64743.
mceachen commentedon Aug 7, 2026
Validated, fixed on main.