Database.prototype.exec calls stackAlloc(4) but never restores the stack pointer. Every exec() permanently consumes 16 bytes (aligned) of the WASM stack, whether it succeeds or throws, regardless of SQL length. run, prepare, each and export are unaffected — I measured each of them with stackSave().
With the default 5.07 MB stack that is a budget of roughly 332,000 exec() calls. Once it is exhausted, the module is corrupted rather than reporting a clean error:
exec #327553 → Error: no such column: value (a query that worked 327k times)
then db.export() → RuntimeError: function signature mismatch
then db.run() → Error: bad parameter or other API misuse
then new SQL.Database() → RuntimeError: memory access out of bounds
then new SQL.Database(bytes) → RuntimeError: table index is out of bounds
Repro (sql.js 1.14.1, Node 24):
const initSqlJs = require("sql.js");
(async () => {
const SQL = await initSqlJs();
const db = new SQL.Database();
db.run("CREATE TABLE t (x); INSERT INTO t VALUES (1);");
const before = SQL.stackSave();
for (let i = 0; i < 1000; i++) db.exec("SELECT x FROM t");
console.log("leaked after 1000 exec():", before - SQL.stackSave()); // 16000
let i = 0;
try { for (;;) { i++; db.exec("SELECT x FROM t"); } }
catch (e) {
console.log(`exec #${i}:`, e.message);
try { db.export(); } catch (e2) { console.log("export():", e2.message); }
try { new SQL.Database(); } catch (e3) { console.log("new Database():", e3.message); }
}
})();
Why it matters: any long-lived process (Electron app, server, editor extension) reaches 332k exec() calls eventually. In our Electron app a single store.get() is one exec(), so it took a few days of uptime; the failure looked like heap/OOM corruption and cost days to diagnose, because memory usage is completely normal at that point (the stack is exhausted, not the heap) and the database file is intact. Restarting the process is the only recovery, since even constructing a fresh Database on the same module fails.
Database.prototype.execcallsstackAlloc(4)but never restores the stack pointer. Everyexec()permanently consumes 16 bytes (aligned) of the WASM stack, whether it succeeds or throws, regardless of SQL length.run,prepare,eachandexportare unaffected — I measured each of them with stackSave().With the default 5.07 MB stack that is a budget of roughly 332,000 exec() calls. Once it is exhausted, the module is corrupted rather than reporting a clean error:
exec #327553 → Error: no such column: value (a query that worked 327k times)
then db.export() → RuntimeError: function signature mismatch
then db.run() → Error: bad parameter or other API misuse
then new SQL.Database() → RuntimeError: memory access out of bounds
then new SQL.Database(bytes) → RuntimeError: table index is out of bounds
Repro (sql.js 1.14.1, Node 24):
Why it matters: any long-lived process (Electron app, server, editor extension) reaches 332k exec() calls eventually. In our Electron app a single store.get() is one exec(), so it took a few days of uptime; the failure looked like heap/OOM corruption and cost days to diagnose, because memory usage is completely normal at that point (the stack is exhausted, not the heap) and the database file is intact. Restarting the process is the only recovery, since even constructing a fresh Database on the same module fails.