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)
Version
all current versions (tested v22.22.2, v24.15.0, v25.9.0, and v26.1.0)
Platform
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 onmain; the other Class A variants are expected to segfault by code inspection — they all funnel through the sameStatementExecutionHelperstep sites.Class A — Statement VM freed mid-step. The original report; affects every StatementSync execution path that runs
sqlite3_step: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. Aftersqlite3_stepreturns,StatementExecutionHelper::Runreadssqlite3_last_insert_rowid(db->Connection())andsqlite3_changes64(db->Connection()). The reentrantdb.close()setdb->connection_ = nullptrbefore step returned, and the bundled SQLite is built withoutSQLITE_ENABLE_API_ARMOR, so passingNULLto those calls is a null-pointer deref: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)