Skip to content

sqlite: introduces RAII ownership for sqlite3_stmt - #62419

Open
araujogui wants to merge 2 commits into
nodejs:mainfrom
araujogui:sqlite-stmt-leak
Open

sqlite: introduces RAII ownership for sqlite3_stmt#62419
araujogui wants to merge 2 commits into
nodejs:mainfrom
araujogui:sqlite-stmt-leak

Conversation

@araujogui

Copy link
Copy Markdown
Member

If StatementSync::Create fails to allocate a JS object (OOM), the sqlite3_stmt prepared by sqlite3_prepare_v2 was leaked and a null pointer was silently inserted into db->statements_

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/sqlite

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. sqlite Issues and PRs related to the SQLite subsystem. labels Mar 24, 2026
@codecov

codecov Bot commented Mar 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.44262% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.24%. Comparing base (f2ba101) to head (78e32bc).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
src/node_sqlite.cc 93.10% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #62419      +/-   ##
==========================================
- Coverage   92.01%   90.24%   -1.77%     
==========================================
  Files         379      739     +360     
  Lines      170129   241673   +71544     
  Branches    26090    45542   +19452     
==========================================
+ Hits       156544   218106   +61562     
- Misses      13293    15110    +1817     
- Partials      292     8457    +8165     
Files with missing lines Coverage Δ
src/node_sqlite.h 81.53% <100.00%> (ø)
src/node_sqlite.cc 80.73% <93.10%> (ø)

... and 487 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@louwers louwers left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can RAII be used instead?

@araujogui

Copy link
Copy Markdown
Member Author

Can RAII be used instead?

Well, we could use std::unique_ptr<sqlite3_stmt, decltype(&sqlite3_finalize)>, but I feel the current way is good enough.

Copilot AI review requested due to automatic review settings July 16, 2026 13:47
@araujogui

Copy link
Copy Markdown
Member Author

@nodejs/sqlite Bump, I did a safety improvement by implementing RAII for sqlite3_stmt.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a resource-management bug in the SQLite sync binding where a prepared sqlite3_stmt could be leaked (and a nullptr inserted into db->statements_) when StatementSync::Create fails to allocate the JS wrapper object (e.g., OOM).

Changes:

  • Introduces RAII ownership for sqlite3_stmt via StatementPtr (DeleteFnPtr + FinalizeStatement).
  • Updates StatementSync to own the prepared statement with StatementPtr and uses .get() at call sites.
  • Ensures DatabaseSync::Prepare returns early when StatementSync::Create fails, preventing nullptr insertion into db->statements_.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/node_sqlite.h Adds FinalizeStatement + StatementPtr alias and updates StatementSync API/member to use RAII for sqlite3_stmt.
src/node_sqlite.cc Transfers statement ownership with StatementPtr, resets via RAII, and guards against Create() failure to avoid leaks/null insertion.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@araujogui araujogui changed the title sqlite: fix sqlite3_stmt leak in prepare create failure sqlite: introduces RAII ownership for sqlite3_stmt Jul 16, 2026

@TrevorBurnham TrevorBurnham left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The statement_statement_.get() conversion checks out. Normalizing it away leaves four semantic edits: the Prepare null guard, the StatementPtr member/ctor/Create signature, Finalize()statement_.reset(), and dropping the two manual sqlite3_finalize(s) calls in SQLTagStore::PrepareStatement. All four look right. The removed finalizes are covered by the by-value StatementPtr parameter's destructor when Create bails, and reset() is idempotent, so the FinalizeStatements() / ~StatementSync double-call path stays safe.

While reviewing I hit two pre-existing bugs in the lines this PR touches. Both predate the change and neither is caused by it, so feel free to split them out — but since this PR is about sqlite3_stmt ownership they seemed worth raising here. Both reproduce on a local build of 78e32bc.

1. Prepare tracks a statement it never untracks (node_sqlite.cc:1551-1561)

sqlite3_prepare_v2 returns SQLITE_OK with *ppStmt == NULL when the input holds no statement. All of '', ' ', '\n', ';', '-- x', '/* x */' are accepted by prepare() on this branch. The new if (!stmt) return; catches Create failing but not s == nullptr, so a StatementSync with statement_ == nullptr gets inserted into db->statements_ at line 1561. When it is GC'd, ~StatementSync checks if (!IsFinalized()) — already true, since IsFinalized() is statement_ == nullptr — so UntrackStatement(this) is skipped and the freed pointer stays in the set. FinalizeStatements() then calls stmt->Finalize() on freed memory at close().

const {DatabaseSync} = require('node:sqlite');
const db1 = new DatabaseSync(':memory:');
const db2 = new DatabaseSync(':memory:');
db2.exec('CREATE TABLE t(a); INSERT INTO t VALUES (1);');
for (let i = 0; i < 200000; i++) db1.prepare('-- ' + i);  // stmt == NULL
const live = [];
for (let i = 0; i < 50000; i++) live.push(db2.prepare('SELECT a FROM t'));
db1.close();  // walks db1->statements_, now full of stale pointers
let broken = 0;
for (const s of live) { try { s.get(); } catch (e) { broken++; } }
console.log(broken, '/', live.length, db2.isOpen);

With --max-old-space-size=80: 19889 / 50000 true. Closing db1 corrupts a different database's live statements into "statement has been finalized" while db2.isOpen is still true. Swapping the comment for real SQL ('SELECT ' + i) gives 0 / 50000, isolating it to the NULL-stmt path.

Fix is either bailing before the insert when s == nullptr, or untracking unconditionally in ~StatementSync.

2. Tag store statements are never tracked (node_sqlite.cc:3572-3583)

SQLTagStore::PrepareStatement puts its StatementSync in sql_tags_ but never does db->statements_.insert(...); line 1561 is the only insert in the file. FinalizeStatements() therefore misses them, sqlite3_close_v2 only defers the close, and the cached statement keeps running against the old connection:

const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE t(a); INSERT INTO t VALUES (111);');
const store = db.createTagStore();
store.get`SELECT a FROM t`;           // { a: 111 }
db.close(); db.open();
db.exec('CREATE TABLE t(a); INSERT INTO t VALUES (222);');
db.prepare('SELECT a FROM t').get();  // { a: 222 }  new connection
store.get`SELECT a FROM t`;           // { a: 111 }  stale

Minor: node_webstorage.h:23 already has stmt_deleter / stmt_unique_ptr for the same job — might be worth one shared definition rather than a second. FinalizeStatement as a free function also reads close to DatabaseSync::FinalizeStatements() and StatementSync::Finalize().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. sqlite Issues and PRs related to the SQLite subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants