sqlite: reject reentry into a running statement - #65106

Closed
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry
Closed

sqlite: reject reentry into a running statement#65106
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes: #65102

SQLite forbids stepping, resetting, or finalizing a statement while that statement's own user-defined function callback is on the stack. The callback depth added in 5cef767 (#64743) is tracked per database, so it cannot distinguish reentry into the running statement from the common pattern of querying a different statement from a callback.

This PR adds a per-statement flag, set for the duration of an execution, and rejects step/reset/finalize on that statement with ERR_INVALID_STATE: statement is currently being executed.

Severity

Reentry corrupts the running virtual machine. Resetting from a callback halts a VM whose sqlite3_step() frame is still live:

// SIGSEGV before this changeletstmt;letfirst=true;db.function('f',()=>{if(first){first=false;stmt.get();}return1;});stmt=db.prepare('SELECT f(), x FROM t');stmt.all();

Verified on v24.15.0 and current main. The crash needs the reentry to be bounded — the repros in #65102 recurse without limit, so V8's stack overflows before control returns into the corrupted step(), which is why that issue concluded it was correctness-only.

reentrant call from callbackbefore
get() / run() / all() (each resets)SIGSEGV
iterator.return() (bare sqlite3_reset)SIGSEGV
aggregate step / result callbackabort, CHECK(agg->initialized) in Release
iterator.next() (steps, no reset)silently consumed rows from the iteration in progress
recursive get(), unboundedMaximum call stack size exceeded
columns() / sourceSQL / expandedSQLunaffected

The aggregate row is a distinct corruption path: the reentrant reset tears down aggregate state mid-xValue, tripping the CHECK in CustomAggregate::DestroyAggregateData. That CHECK is compiled into release builds, so it aborts in production rather than only under a debug build.

Covered entry points: all(), get(), run(), iterate(), iterator.next(), iterator.return(), close(), [Symbol.dispose](), and the four SQL tag store methods. close() and [Symbol.dispose]() are included because finalizing mid-step frees the virtual machine that sqlite3_step() is still executing.

Prior art: #63183 took this approach alongside its own database-level guard. That PR was closed once #64743 landed, so this salvages the per-statement half and builds on the guard already in main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/sqlite

@nodejs-github-botnodejs-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 Aug 7, 2026
@codecov

codecovBot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.53846% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.32%. Comparing base (e2d7b34) to head (abab573).
⚠️ Report is 62 commits behind head on main.

Files with missing linesPatch %Lines
src/node_sqlite.cc54.54%4 Missing and 6 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65106 +/- ##
========================================
Coverage 90.31% 90.32% ========================================
Files 759 760 +1 Lines 248290 248551 +261 Branches 46859 46918 +59 ========================================
+ Hits 224241 224501 +260 + Misses 15472 15460 -12 - Partials 8577 8590 +13 
Files with missing linesCoverage Δ
src/node_sqlite.h83.56% <100.00%> (+0.95%)⬆️
src/node_sqlite.cc81.24% <54.54%> (+0.18%)⬆️

... and 69 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.

@trivikr

This comment was marked as outdated.

@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from ee4e9b5 to 986eb0eCompareAugust 7, 2026 17:41
@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

@trivikr I've addressed the issues you noted. The format-cpp check is now timing out after 15 minutes ("The operation was canceled"); the same check succeeds locally.

Comment threadsrc/node_sqlite.cc
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 986eb0e to fde8f11CompareAugust 8, 2026 12:09
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js Outdated
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from fde8f11 to 02e92f5CompareAugust 8, 2026 14:58
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 9, 2026
@trivikrtrivikr added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 9, 2026
@avivkelleravivkeller added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 9, 2026
@avivkeller

Copy link
Copy Markdown
Member
Commit Queue failed
- Loading data for nodejs/node/pull/65106
✔ Done loading data for nodejs/node/pull/65106
----------------------------------- PR info ------------------------------------
Title sqlite: reject reentry into a running statement (#65106)
Author Trevor Burnham <trevorburnham@gmail.com> (@TrevorBurnham)
Branch TrevorBurnham:sqlite/guard-statement-reentry -> nodejs:main
Labels c++, author ready, needs-ci, commit-queue, sqlite
Commits 1
- sqlite: reject reentry into a running statement
Committers 1
- Trevor Burnham <trevorburnham@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Fri, 07 Aug 2026 14:58:01 GMT
✔ Approvals: 1
✔ - Trivikram Kamat (@trivikr): https://github.com/nodejs/node/pull/65106#pullrequestreview-4889248717
✘ This PR needs to wait 113 more hours to land (or 0 minutes if there is one more approval)
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-08T17:08:25Z: https://ci.nodejs.org/job/node-test-pull-request/75659/
- Querying data for job/node-test-pull-request/75659/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /Users/avivkeller/Documents/projects/nodejs/node/.ncu
/nodejs/node/actions/runs/

@avivkelleravivkeller added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 9, 2026
SQLite forbids stepping, resetting, or finalizing a statement while
that statement's own user-defined function callback is on the stack.
The callback depth added in 5cef767 is tracked per database, so
it cannot tell reentry into the running statement apart from the
common pattern of querying a different statement from a callback.
Mark the statement being executed and reject step, reset, and
finalize on that statement with ERR_INVALID_STATE. Statements other
than the running one are unaffected.
Reentry corrupts the running virtual machine rather than merely
producing a wrong answer. Resetting from a callback halts a VM whose
sqlite3_step() frame is still live, so a bounded reentrant get() or
run(), and iterator.return() with its bare sqlite3_reset(), all
segfault; from an aggregate's step or result callback the same reset
tears down aggregate state mid-xValue and aborts on a CHECK in a
release build. Where it does not crash it is still wrong: a
reentrant iterator.next() silently consumed rows from the iteration
in progress, and a recursive get() surfaced a V8 stack overflow
instead of the constraint. close() and [Symbol.dispose]() are
covered too, since finalizing mid-step frees the running VM.
The mark is set before parameters are bound, so a getter or valueOf()
that reenters while its own arguments are being evaluated is rejected
as well. Without this, two iterators could share one virtual machine
and interleave rows from a single result set.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Assisted-by: claude:opus-5
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 02e92f5 to abab573CompareAugust 10, 2026 13:36
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@nodejs-github-bot

This comment was marked as resolved.

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

^ Updated the PR to cover sqlite3_reset. The PR description has been updated as well.

@panvapanva removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 10, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

This PR was largely superseded by #65156. Closing it out. I've spun off one small follow-up PR: #65294

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

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.needs-ciPRs that need a full CI run.sqliteIssues and PRs related to the SQLite subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sqlite: reentrancy into a running statement from a user-defined function is unguarded

5 participants

@TrevorBurnham@nodejs-github-bot@trivikr@avivkeller@panva
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

sqlite: reject reentry into a running statement - #65106

Closed
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry
Closed

sqlite: reject reentry into a running statement#65106
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes: #65102

SQLite forbids stepping, resetting, or finalizing a statement while that statement's own user-defined function callback is on the stack. The callback depth added in 5cef767 (#64743) is tracked per database, so it cannot distinguish reentry into the running statement from the common pattern of querying a different statement from a callback.

This PR adds a per-statement flag, set for the duration of an execution, and rejects step/reset/finalize on that statement with ERR_INVALID_STATE: statement is currently being executed.

Severity

Reentry corrupts the running virtual machine. Resetting from a callback halts a VM whose sqlite3_step() frame is still live:

// SIGSEGV before this changeletstmt;letfirst=true;db.function('f',()=>{if(first){first=false;stmt.get();}return1;});stmt=db.prepare('SELECT f(), x FROM t');stmt.all();

Verified on v24.15.0 and current main. The crash needs the reentry to be bounded — the repros in #65102 recurse without limit, so V8's stack overflows before control returns into the corrupted step(), which is why that issue concluded it was correctness-only.

reentrant call from callbackbefore
get() / run() / all() (each resets)SIGSEGV
iterator.return() (bare sqlite3_reset)SIGSEGV
aggregate step / result callbackabort, CHECK(agg->initialized) in Release
iterator.next() (steps, no reset)silently consumed rows from the iteration in progress
recursive get(), unboundedMaximum call stack size exceeded
columns() / sourceSQL / expandedSQLunaffected

The aggregate row is a distinct corruption path: the reentrant reset tears down aggregate state mid-xValue, tripping the CHECK in CustomAggregate::DestroyAggregateData. That CHECK is compiled into release builds, so it aborts in production rather than only under a debug build.

Covered entry points: all(), get(), run(), iterate(), iterator.next(), iterator.return(), close(), [Symbol.dispose](), and the four SQL tag store methods. close() and [Symbol.dispose]() are included because finalizing mid-step frees the virtual machine that sqlite3_step() is still executing.

Prior art: #63183 took this approach alongside its own database-level guard. That PR was closed once #64743 landed, so this salvages the per-statement half and builds on the guard already in main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/sqlite

@nodejs-github-botnodejs-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 Aug 7, 2026
@codecov

codecovBot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.53846% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.32%. Comparing base (e2d7b34) to head (abab573).
⚠️ Report is 62 commits behind head on main.

Files with missing linesPatch %Lines
src/node_sqlite.cc54.54%4 Missing and 6 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65106 +/- ##
========================================
Coverage 90.31% 90.32% ========================================
Files 759 760 +1 Lines 248290 248551 +261 Branches 46859 46918 +59 ========================================
+ Hits 224241 224501 +260 + Misses 15472 15460 -12 - Partials 8577 8590 +13 
Files with missing linesCoverage Δ
src/node_sqlite.h83.56% <100.00%> (+0.95%)⬆️
src/node_sqlite.cc81.24% <54.54%> (+0.18%)⬆️

... and 69 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.

@trivikr

This comment was marked as outdated.

@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from ee4e9b5 to 986eb0eCompareAugust 7, 2026 17:41
@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

@trivikr I've addressed the issues you noted. The format-cpp check is now timing out after 15 minutes ("The operation was canceled"); the same check succeeds locally.

Comment threadsrc/node_sqlite.cc
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 986eb0e to fde8f11CompareAugust 8, 2026 12:09
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js Outdated
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from fde8f11 to 02e92f5CompareAugust 8, 2026 14:58
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 9, 2026
@trivikrtrivikr added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 9, 2026
@avivkelleravivkeller added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 9, 2026
@avivkeller

Copy link
Copy Markdown
Member
Commit Queue failed
- Loading data for nodejs/node/pull/65106
✔ Done loading data for nodejs/node/pull/65106
----------------------------------- PR info ------------------------------------
Title sqlite: reject reentry into a running statement (#65106)
Author Trevor Burnham <trevorburnham@gmail.com> (@TrevorBurnham)
Branch TrevorBurnham:sqlite/guard-statement-reentry -> nodejs:main
Labels c++, author ready, needs-ci, commit-queue, sqlite
Commits 1
- sqlite: reject reentry into a running statement
Committers 1
- Trevor Burnham <trevorburnham@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Fri, 07 Aug 2026 14:58:01 GMT
✔ Approvals: 1
✔ - Trivikram Kamat (@trivikr): https://github.com/nodejs/node/pull/65106#pullrequestreview-4889248717
✘ This PR needs to wait 113 more hours to land (or 0 minutes if there is one more approval)
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-08T17:08:25Z: https://ci.nodejs.org/job/node-test-pull-request/75659/
- Querying data for job/node-test-pull-request/75659/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /Users/avivkeller/Documents/projects/nodejs/node/.ncu
/nodejs/node/actions/runs/

@avivkelleravivkeller added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 9, 2026
SQLite forbids stepping, resetting, or finalizing a statement while
that statement's own user-defined function callback is on the stack.
The callback depth added in 5cef767 is tracked per database, so
it cannot tell reentry into the running statement apart from the
common pattern of querying a different statement from a callback.
Mark the statement being executed and reject step, reset, and
finalize on that statement with ERR_INVALID_STATE. Statements other
than the running one are unaffected.
Reentry corrupts the running virtual machine rather than merely
producing a wrong answer. Resetting from a callback halts a VM whose
sqlite3_step() frame is still live, so a bounded reentrant get() or
run(), and iterator.return() with its bare sqlite3_reset(), all
segfault; from an aggregate's step or result callback the same reset
tears down aggregate state mid-xValue and aborts on a CHECK in a
release build. Where it does not crash it is still wrong: a
reentrant iterator.next() silently consumed rows from the iteration
in progress, and a recursive get() surfaced a V8 stack overflow
instead of the constraint. close() and [Symbol.dispose]() are
covered too, since finalizing mid-step frees the running VM.
The mark is set before parameters are bound, so a getter or valueOf()
that reenters while its own arguments are being evaluated is rejected
as well. Without this, two iterators could share one virtual machine
and interleave rows from a single result set.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Assisted-by: claude:opus-5
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 02e92f5 to abab573CompareAugust 10, 2026 13:36
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@nodejs-github-bot

This comment was marked as resolved.

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

^ Updated the PR to cover sqlite3_reset. The PR description has been updated as well.

@panvapanva removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 10, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

This PR was largely superseded by #65156. Closing it out. I've spun off one small follow-up PR: #65294

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

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.needs-ciPRs that need a full CI run.sqliteIssues and PRs related to the SQLite subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sqlite: reentrancy into a running statement from a user-defined function is unguarded

5 participants

@TrevorBurnham@nodejs-github-bot@trivikr@avivkeller@panva
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

sqlite: reject reentry into a running statement - #65106

Closed
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry
Closed

sqlite: reject reentry into a running statement#65106
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes: #65102

SQLite forbids stepping, resetting, or finalizing a statement while that statement's own user-defined function callback is on the stack. The callback depth added in 5cef767 (#64743) is tracked per database, so it cannot distinguish reentry into the running statement from the common pattern of querying a different statement from a callback.

This PR adds a per-statement flag, set for the duration of an execution, and rejects step/reset/finalize on that statement with ERR_INVALID_STATE: statement is currently being executed.

Severity

Reentry corrupts the running virtual machine. Resetting from a callback halts a VM whose sqlite3_step() frame is still live:

// SIGSEGV before this changeletstmt;letfirst=true;db.function('f',()=>{if(first){first=false;stmt.get();}return1;});stmt=db.prepare('SELECT f(), x FROM t');stmt.all();

Verified on v24.15.0 and current main. The crash needs the reentry to be bounded — the repros in #65102 recurse without limit, so V8's stack overflows before control returns into the corrupted step(), which is why that issue concluded it was correctness-only.

reentrant call from callbackbefore
get() / run() / all() (each resets)SIGSEGV
iterator.return() (bare sqlite3_reset)SIGSEGV
aggregate step / result callbackabort, CHECK(agg->initialized) in Release
iterator.next() (steps, no reset)silently consumed rows from the iteration in progress
recursive get(), unboundedMaximum call stack size exceeded
columns() / sourceSQL / expandedSQLunaffected

The aggregate row is a distinct corruption path: the reentrant reset tears down aggregate state mid-xValue, tripping the CHECK in CustomAggregate::DestroyAggregateData. That CHECK is compiled into release builds, so it aborts in production rather than only under a debug build.

Covered entry points: all(), get(), run(), iterate(), iterator.next(), iterator.return(), close(), [Symbol.dispose](), and the four SQL tag store methods. close() and [Symbol.dispose]() are included because finalizing mid-step frees the virtual machine that sqlite3_step() is still executing.

Prior art: #63183 took this approach alongside its own database-level guard. That PR was closed once #64743 landed, so this salvages the per-statement half and builds on the guard already in main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/sqlite

@nodejs-github-botnodejs-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 Aug 7, 2026
@codecov

codecovBot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.53846% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.32%. Comparing base (e2d7b34) to head (abab573).
⚠️ Report is 62 commits behind head on main.

Files with missing linesPatch %Lines
src/node_sqlite.cc54.54%4 Missing and 6 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65106 +/- ##
========================================
Coverage 90.31% 90.32% ========================================
Files 759 760 +1 Lines 248290 248551 +261 Branches 46859 46918 +59 ========================================
+ Hits 224241 224501 +260 + Misses 15472 15460 -12 - Partials 8577 8590 +13 
Files with missing linesCoverage Δ
src/node_sqlite.h83.56% <100.00%> (+0.95%)⬆️
src/node_sqlite.cc81.24% <54.54%> (+0.18%)⬆️

... and 69 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.

@trivikr

This comment was marked as outdated.

@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from ee4e9b5 to 986eb0eCompareAugust 7, 2026 17:41
@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

@trivikr I've addressed the issues you noted. The format-cpp check is now timing out after 15 minutes ("The operation was canceled"); the same check succeeds locally.

Comment threadsrc/node_sqlite.cc
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 986eb0e to fde8f11CompareAugust 8, 2026 12:09
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js Outdated
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from fde8f11 to 02e92f5CompareAugust 8, 2026 14:58
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 9, 2026
@trivikrtrivikr added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 9, 2026
@avivkelleravivkeller added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 9, 2026
@avivkeller

Copy link
Copy Markdown
Member
Commit Queue failed
- Loading data for nodejs/node/pull/65106
✔ Done loading data for nodejs/node/pull/65106
----------------------------------- PR info ------------------------------------
Title sqlite: reject reentry into a running statement (#65106)
Author Trevor Burnham <trevorburnham@gmail.com> (@TrevorBurnham)
Branch TrevorBurnham:sqlite/guard-statement-reentry -> nodejs:main
Labels c++, author ready, needs-ci, commit-queue, sqlite
Commits 1
- sqlite: reject reentry into a running statement
Committers 1
- Trevor Burnham <trevorburnham@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Fri, 07 Aug 2026 14:58:01 GMT
✔ Approvals: 1
✔ - Trivikram Kamat (@trivikr): https://github.com/nodejs/node/pull/65106#pullrequestreview-4889248717
✘ This PR needs to wait 113 more hours to land (or 0 minutes if there is one more approval)
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-08T17:08:25Z: https://ci.nodejs.org/job/node-test-pull-request/75659/
- Querying data for job/node-test-pull-request/75659/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /Users/avivkeller/Documents/projects/nodejs/node/.ncu
/nodejs/node/actions/runs/

@avivkelleravivkeller added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 9, 2026
SQLite forbids stepping, resetting, or finalizing a statement while
that statement's own user-defined function callback is on the stack.
The callback depth added in 5cef767 is tracked per database, so
it cannot tell reentry into the running statement apart from the
common pattern of querying a different statement from a callback.
Mark the statement being executed and reject step, reset, and
finalize on that statement with ERR_INVALID_STATE. Statements other
than the running one are unaffected.
Reentry corrupts the running virtual machine rather than merely
producing a wrong answer. Resetting from a callback halts a VM whose
sqlite3_step() frame is still live, so a bounded reentrant get() or
run(), and iterator.return() with its bare sqlite3_reset(), all
segfault; from an aggregate's step or result callback the same reset
tears down aggregate state mid-xValue and aborts on a CHECK in a
release build. Where it does not crash it is still wrong: a
reentrant iterator.next() silently consumed rows from the iteration
in progress, and a recursive get() surfaced a V8 stack overflow
instead of the constraint. close() and [Symbol.dispose]() are
covered too, since finalizing mid-step frees the running VM.
The mark is set before parameters are bound, so a getter or valueOf()
that reenters while its own arguments are being evaluated is rejected
as well. Without this, two iterators could share one virtual machine
and interleave rows from a single result set.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Assisted-by: claude:opus-5
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 02e92f5 to abab573CompareAugust 10, 2026 13:36
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@nodejs-github-bot

This comment was marked as resolved.

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

^ Updated the PR to cover sqlite3_reset. The PR description has been updated as well.

@panvapanva removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 10, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

This PR was largely superseded by #65156. Closing it out. I've spun off one small follow-up PR: #65294

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

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.needs-ciPRs that need a full CI run.sqliteIssues and PRs related to the SQLite subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sqlite: reentrancy into a running statement from a user-defined function is unguarded

5 participants

@TrevorBurnham@nodejs-github-bot@trivikr@avivkeller@panva
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

sqlite: reject reentry into a running statement - #65106

Closed
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry
Closed

sqlite: reject reentry into a running statement#65106
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes: #65102

SQLite forbids stepping, resetting, or finalizing a statement while that statement's own user-defined function callback is on the stack. The callback depth added in 5cef767 (#64743) is tracked per database, so it cannot distinguish reentry into the running statement from the common pattern of querying a different statement from a callback.

This PR adds a per-statement flag, set for the duration of an execution, and rejects step/reset/finalize on that statement with ERR_INVALID_STATE: statement is currently being executed.

Severity

Reentry corrupts the running virtual machine. Resetting from a callback halts a VM whose sqlite3_step() frame is still live:

// SIGSEGV before this changeletstmt;letfirst=true;db.function('f',()=>{if(first){first=false;stmt.get();}return1;});stmt=db.prepare('SELECT f(), x FROM t');stmt.all();

Verified on v24.15.0 and current main. The crash needs the reentry to be bounded — the repros in #65102 recurse without limit, so V8's stack overflows before control returns into the corrupted step(), which is why that issue concluded it was correctness-only.

reentrant call from callbackbefore
get() / run() / all() (each resets)SIGSEGV
iterator.return() (bare sqlite3_reset)SIGSEGV
aggregate step / result callbackabort, CHECK(agg->initialized) in Release
iterator.next() (steps, no reset)silently consumed rows from the iteration in progress
recursive get(), unboundedMaximum call stack size exceeded
columns() / sourceSQL / expandedSQLunaffected

The aggregate row is a distinct corruption path: the reentrant reset tears down aggregate state mid-xValue, tripping the CHECK in CustomAggregate::DestroyAggregateData. That CHECK is compiled into release builds, so it aborts in production rather than only under a debug build.

Covered entry points: all(), get(), run(), iterate(), iterator.next(), iterator.return(), close(), [Symbol.dispose](), and the four SQL tag store methods. close() and [Symbol.dispose]() are included because finalizing mid-step frees the virtual machine that sqlite3_step() is still executing.

Prior art: #63183 took this approach alongside its own database-level guard. That PR was closed once #64743 landed, so this salvages the per-statement half and builds on the guard already in main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/sqlite

@nodejs-github-botnodejs-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 Aug 7, 2026
@codecov

codecovBot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.53846% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.32%. Comparing base (e2d7b34) to head (abab573).
⚠️ Report is 62 commits behind head on main.

Files with missing linesPatch %Lines
src/node_sqlite.cc54.54%4 Missing and 6 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65106 +/- ##
========================================
Coverage 90.31% 90.32% ========================================
Files 759 760 +1 Lines 248290 248551 +261 Branches 46859 46918 +59 ========================================
+ Hits 224241 224501 +260 + Misses 15472 15460 -12 - Partials 8577 8590 +13 
Files with missing linesCoverage Δ
src/node_sqlite.h83.56% <100.00%> (+0.95%)⬆️
src/node_sqlite.cc81.24% <54.54%> (+0.18%)⬆️

... and 69 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.

@trivikr

This comment was marked as outdated.

@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from ee4e9b5 to 986eb0eCompareAugust 7, 2026 17:41
@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

@trivikr I've addressed the issues you noted. The format-cpp check is now timing out after 15 minutes ("The operation was canceled"); the same check succeeds locally.

Comment threadsrc/node_sqlite.cc
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 986eb0e to fde8f11CompareAugust 8, 2026 12:09
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js Outdated
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from fde8f11 to 02e92f5CompareAugust 8, 2026 14:58
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 9, 2026
@trivikrtrivikr added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 9, 2026
@avivkelleravivkeller added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 9, 2026
@avivkeller

Copy link
Copy Markdown
Member
Commit Queue failed
- Loading data for nodejs/node/pull/65106
✔ Done loading data for nodejs/node/pull/65106
----------------------------------- PR info ------------------------------------
Title sqlite: reject reentry into a running statement (#65106)
Author Trevor Burnham <trevorburnham@gmail.com> (@TrevorBurnham)
Branch TrevorBurnham:sqlite/guard-statement-reentry -> nodejs:main
Labels c++, author ready, needs-ci, commit-queue, sqlite
Commits 1
- sqlite: reject reentry into a running statement
Committers 1
- Trevor Burnham <trevorburnham@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Fri, 07 Aug 2026 14:58:01 GMT
✔ Approvals: 1
✔ - Trivikram Kamat (@trivikr): https://github.com/nodejs/node/pull/65106#pullrequestreview-4889248717
✘ This PR needs to wait 113 more hours to land (or 0 minutes if there is one more approval)
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-08T17:08:25Z: https://ci.nodejs.org/job/node-test-pull-request/75659/
- Querying data for job/node-test-pull-request/75659/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /Users/avivkeller/Documents/projects/nodejs/node/.ncu
/nodejs/node/actions/runs/

@avivkelleravivkeller added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 9, 2026
SQLite forbids stepping, resetting, or finalizing a statement while
that statement's own user-defined function callback is on the stack.
The callback depth added in 5cef767 is tracked per database, so
it cannot tell reentry into the running statement apart from the
common pattern of querying a different statement from a callback.
Mark the statement being executed and reject step, reset, and
finalize on that statement with ERR_INVALID_STATE. Statements other
than the running one are unaffected.
Reentry corrupts the running virtual machine rather than merely
producing a wrong answer. Resetting from a callback halts a VM whose
sqlite3_step() frame is still live, so a bounded reentrant get() or
run(), and iterator.return() with its bare sqlite3_reset(), all
segfault; from an aggregate's step or result callback the same reset
tears down aggregate state mid-xValue and aborts on a CHECK in a
release build. Where it does not crash it is still wrong: a
reentrant iterator.next() silently consumed rows from the iteration
in progress, and a recursive get() surfaced a V8 stack overflow
instead of the constraint. close() and [Symbol.dispose]() are
covered too, since finalizing mid-step frees the running VM.
The mark is set before parameters are bound, so a getter or valueOf()
that reenters while its own arguments are being evaluated is rejected
as well. Without this, two iterators could share one virtual machine
and interleave rows from a single result set.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Assisted-by: claude:opus-5
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 02e92f5 to abab573CompareAugust 10, 2026 13:36
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@nodejs-github-bot

This comment was marked as resolved.

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

^ Updated the PR to cover sqlite3_reset. The PR description has been updated as well.

@panvapanva removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 10, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

This PR was largely superseded by #65156. Closing it out. I've spun off one small follow-up PR: #65294

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

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.needs-ciPRs that need a full CI run.sqliteIssues and PRs related to the SQLite subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sqlite: reentrancy into a running statement from a user-defined function is unguarded

5 participants

@TrevorBurnham@nodejs-github-bot@trivikr@avivkeller@panva
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

sqlite: reject reentry into a running statement - #65106

Closed
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry
Closed

sqlite: reject reentry into a running statement#65106
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes: #65102

SQLite forbids stepping, resetting, or finalizing a statement while that statement's own user-defined function callback is on the stack. The callback depth added in 5cef767 (#64743) is tracked per database, so it cannot distinguish reentry into the running statement from the common pattern of querying a different statement from a callback.

This PR adds a per-statement flag, set for the duration of an execution, and rejects step/reset/finalize on that statement with ERR_INVALID_STATE: statement is currently being executed.

Severity

Reentry corrupts the running virtual machine. Resetting from a callback halts a VM whose sqlite3_step() frame is still live:

// SIGSEGV before this changeletstmt;letfirst=true;db.function('f',()=>{if(first){first=false;stmt.get();}return1;});stmt=db.prepare('SELECT f(), x FROM t');stmt.all();

Verified on v24.15.0 and current main. The crash needs the reentry to be bounded — the repros in #65102 recurse without limit, so V8's stack overflows before control returns into the corrupted step(), which is why that issue concluded it was correctness-only.

reentrant call from callbackbefore
get() / run() / all() (each resets)SIGSEGV
iterator.return() (bare sqlite3_reset)SIGSEGV
aggregate step / result callbackabort, CHECK(agg->initialized) in Release
iterator.next() (steps, no reset)silently consumed rows from the iteration in progress
recursive get(), unboundedMaximum call stack size exceeded
columns() / sourceSQL / expandedSQLunaffected

The aggregate row is a distinct corruption path: the reentrant reset tears down aggregate state mid-xValue, tripping the CHECK in CustomAggregate::DestroyAggregateData. That CHECK is compiled into release builds, so it aborts in production rather than only under a debug build.

Covered entry points: all(), get(), run(), iterate(), iterator.next(), iterator.return(), close(), [Symbol.dispose](), and the four SQL tag store methods. close() and [Symbol.dispose]() are included because finalizing mid-step frees the virtual machine that sqlite3_step() is still executing.

Prior art: #63183 took this approach alongside its own database-level guard. That PR was closed once #64743 landed, so this salvages the per-statement half and builds on the guard already in main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/sqlite

@nodejs-github-botnodejs-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 Aug 7, 2026
@codecov

codecovBot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.53846% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.32%. Comparing base (e2d7b34) to head (abab573).
⚠️ Report is 62 commits behind head on main.

Files with missing linesPatch %Lines
src/node_sqlite.cc54.54%4 Missing and 6 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65106 +/- ##
========================================
Coverage 90.31% 90.32% ========================================
Files 759 760 +1 Lines 248290 248551 +261 Branches 46859 46918 +59 ========================================
+ Hits 224241 224501 +260 + Misses 15472 15460 -12 - Partials 8577 8590 +13 
Files with missing linesCoverage Δ
src/node_sqlite.h83.56% <100.00%> (+0.95%)⬆️
src/node_sqlite.cc81.24% <54.54%> (+0.18%)⬆️

... and 69 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.

@trivikr

This comment was marked as outdated.

@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from ee4e9b5 to 986eb0eCompareAugust 7, 2026 17:41
@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

@trivikr I've addressed the issues you noted. The format-cpp check is now timing out after 15 minutes ("The operation was canceled"); the same check succeeds locally.

Comment threadsrc/node_sqlite.cc
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 986eb0e to fde8f11CompareAugust 8, 2026 12:09
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js Outdated
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from fde8f11 to 02e92f5CompareAugust 8, 2026 14:58
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 9, 2026
@trivikrtrivikr added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 9, 2026
@avivkelleravivkeller added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 9, 2026
@avivkeller

Copy link
Copy Markdown
Member
Commit Queue failed
- Loading data for nodejs/node/pull/65106
✔ Done loading data for nodejs/node/pull/65106
----------------------------------- PR info ------------------------------------
Title sqlite: reject reentry into a running statement (#65106)
Author Trevor Burnham <trevorburnham@gmail.com> (@TrevorBurnham)
Branch TrevorBurnham:sqlite/guard-statement-reentry -> nodejs:main
Labels c++, author ready, needs-ci, commit-queue, sqlite
Commits 1
- sqlite: reject reentry into a running statement
Committers 1
- Trevor Burnham <trevorburnham@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Fri, 07 Aug 2026 14:58:01 GMT
✔ Approvals: 1
✔ - Trivikram Kamat (@trivikr): https://github.com/nodejs/node/pull/65106#pullrequestreview-4889248717
✘ This PR needs to wait 113 more hours to land (or 0 minutes if there is one more approval)
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-08T17:08:25Z: https://ci.nodejs.org/job/node-test-pull-request/75659/
- Querying data for job/node-test-pull-request/75659/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /Users/avivkeller/Documents/projects/nodejs/node/.ncu
/nodejs/node/actions/runs/

@avivkelleravivkeller added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 9, 2026
SQLite forbids stepping, resetting, or finalizing a statement while
that statement's own user-defined function callback is on the stack.
The callback depth added in 5cef767 is tracked per database, so
it cannot tell reentry into the running statement apart from the
common pattern of querying a different statement from a callback.
Mark the statement being executed and reject step, reset, and
finalize on that statement with ERR_INVALID_STATE. Statements other
than the running one are unaffected.
Reentry corrupts the running virtual machine rather than merely
producing a wrong answer. Resetting from a callback halts a VM whose
sqlite3_step() frame is still live, so a bounded reentrant get() or
run(), and iterator.return() with its bare sqlite3_reset(), all
segfault; from an aggregate's step or result callback the same reset
tears down aggregate state mid-xValue and aborts on a CHECK in a
release build. Where it does not crash it is still wrong: a
reentrant iterator.next() silently consumed rows from the iteration
in progress, and a recursive get() surfaced a V8 stack overflow
instead of the constraint. close() and [Symbol.dispose]() are
covered too, since finalizing mid-step frees the running VM.
The mark is set before parameters are bound, so a getter or valueOf()
that reenters while its own arguments are being evaluated is rejected
as well. Without this, two iterators could share one virtual machine
and interleave rows from a single result set.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Assisted-by: claude:opus-5
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 02e92f5 to abab573CompareAugust 10, 2026 13:36
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@nodejs-github-bot

This comment was marked as resolved.

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

^ Updated the PR to cover sqlite3_reset. The PR description has been updated as well.

@panvapanva removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 10, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

This PR was largely superseded by #65156. Closing it out. I've spun off one small follow-up PR: #65294

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

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.needs-ciPRs that need a full CI run.sqliteIssues and PRs related to the SQLite subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sqlite: reentrancy into a running statement from a user-defined function is unguarded

5 participants

@TrevorBurnham@nodejs-github-bot@trivikr@avivkeller@panva
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

sqlite: reject reentry into a running statement - #65106

Closed
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry
Closed

sqlite: reject reentry into a running statement#65106
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes: #65102

SQLite forbids stepping, resetting, or finalizing a statement while that statement's own user-defined function callback is on the stack. The callback depth added in 5cef767 (#64743) is tracked per database, so it cannot distinguish reentry into the running statement from the common pattern of querying a different statement from a callback.

This PR adds a per-statement flag, set for the duration of an execution, and rejects step/reset/finalize on that statement with ERR_INVALID_STATE: statement is currently being executed.

Severity

Reentry corrupts the running virtual machine. Resetting from a callback halts a VM whose sqlite3_step() frame is still live:

// SIGSEGV before this changeletstmt;letfirst=true;db.function('f',()=>{if(first){first=false;stmt.get();}return1;});stmt=db.prepare('SELECT f(), x FROM t');stmt.all();

Verified on v24.15.0 and current main. The crash needs the reentry to be bounded — the repros in #65102 recurse without limit, so V8's stack overflows before control returns into the corrupted step(), which is why that issue concluded it was correctness-only.

reentrant call from callbackbefore
get() / run() / all() (each resets)SIGSEGV
iterator.return() (bare sqlite3_reset)SIGSEGV
aggregate step / result callbackabort, CHECK(agg->initialized) in Release
iterator.next() (steps, no reset)silently consumed rows from the iteration in progress
recursive get(), unboundedMaximum call stack size exceeded
columns() / sourceSQL / expandedSQLunaffected

The aggregate row is a distinct corruption path: the reentrant reset tears down aggregate state mid-xValue, tripping the CHECK in CustomAggregate::DestroyAggregateData. That CHECK is compiled into release builds, so it aborts in production rather than only under a debug build.

Covered entry points: all(), get(), run(), iterate(), iterator.next(), iterator.return(), close(), [Symbol.dispose](), and the four SQL tag store methods. close() and [Symbol.dispose]() are included because finalizing mid-step frees the virtual machine that sqlite3_step() is still executing.

Prior art: #63183 took this approach alongside its own database-level guard. That PR was closed once #64743 landed, so this salvages the per-statement half and builds on the guard already in main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/sqlite

@nodejs-github-botnodejs-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 Aug 7, 2026
@codecov

codecovBot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.53846% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.32%. Comparing base (e2d7b34) to head (abab573).
⚠️ Report is 62 commits behind head on main.

Files with missing linesPatch %Lines
src/node_sqlite.cc54.54%4 Missing and 6 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65106 +/- ##
========================================
Coverage 90.31% 90.32% ========================================
Files 759 760 +1 Lines 248290 248551 +261 Branches 46859 46918 +59 ========================================
+ Hits 224241 224501 +260 + Misses 15472 15460 -12 - Partials 8577 8590 +13 
Files with missing linesCoverage Δ
src/node_sqlite.h83.56% <100.00%> (+0.95%)⬆️
src/node_sqlite.cc81.24% <54.54%> (+0.18%)⬆️

... and 69 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.

@trivikr

This comment was marked as outdated.

@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from ee4e9b5 to 986eb0eCompareAugust 7, 2026 17:41
@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

@trivikr I've addressed the issues you noted. The format-cpp check is now timing out after 15 minutes ("The operation was canceled"); the same check succeeds locally.

Comment threadsrc/node_sqlite.cc
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 986eb0e to fde8f11CompareAugust 8, 2026 12:09
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js Outdated
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from fde8f11 to 02e92f5CompareAugust 8, 2026 14:58
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 9, 2026
@trivikrtrivikr added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 9, 2026
@avivkelleravivkeller added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 9, 2026
@avivkeller

Copy link
Copy Markdown
Member
Commit Queue failed
- Loading data for nodejs/node/pull/65106
✔ Done loading data for nodejs/node/pull/65106
----------------------------------- PR info ------------------------------------
Title sqlite: reject reentry into a running statement (#65106)
Author Trevor Burnham <trevorburnham@gmail.com> (@TrevorBurnham)
Branch TrevorBurnham:sqlite/guard-statement-reentry -> nodejs:main
Labels c++, author ready, needs-ci, commit-queue, sqlite
Commits 1
- sqlite: reject reentry into a running statement
Committers 1
- Trevor Burnham <trevorburnham@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Fri, 07 Aug 2026 14:58:01 GMT
✔ Approvals: 1
✔ - Trivikram Kamat (@trivikr): https://github.com/nodejs/node/pull/65106#pullrequestreview-4889248717
✘ This PR needs to wait 113 more hours to land (or 0 minutes if there is one more approval)
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-08T17:08:25Z: https://ci.nodejs.org/job/node-test-pull-request/75659/
- Querying data for job/node-test-pull-request/75659/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /Users/avivkeller/Documents/projects/nodejs/node/.ncu
/nodejs/node/actions/runs/

@avivkelleravivkeller added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 9, 2026
SQLite forbids stepping, resetting, or finalizing a statement while
that statement's own user-defined function callback is on the stack.
The callback depth added in 5cef767 is tracked per database, so
it cannot tell reentry into the running statement apart from the
common pattern of querying a different statement from a callback.
Mark the statement being executed and reject step, reset, and
finalize on that statement with ERR_INVALID_STATE. Statements other
than the running one are unaffected.
Reentry corrupts the running virtual machine rather than merely
producing a wrong answer. Resetting from a callback halts a VM whose
sqlite3_step() frame is still live, so a bounded reentrant get() or
run(), and iterator.return() with its bare sqlite3_reset(), all
segfault; from an aggregate's step or result callback the same reset
tears down aggregate state mid-xValue and aborts on a CHECK in a
release build. Where it does not crash it is still wrong: a
reentrant iterator.next() silently consumed rows from the iteration
in progress, and a recursive get() surfaced a V8 stack overflow
instead of the constraint. close() and [Symbol.dispose]() are
covered too, since finalizing mid-step frees the running VM.
The mark is set before parameters are bound, so a getter or valueOf()
that reenters while its own arguments are being evaluated is rejected
as well. Without this, two iterators could share one virtual machine
and interleave rows from a single result set.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Assisted-by: claude:opus-5
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 02e92f5 to abab573CompareAugust 10, 2026 13:36
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@nodejs-github-bot

This comment was marked as resolved.

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

^ Updated the PR to cover sqlite3_reset. The PR description has been updated as well.

@panvapanva removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 10, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

This PR was largely superseded by #65156. Closing it out. I've spun off one small follow-up PR: #65294

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

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.needs-ciPRs that need a full CI run.sqliteIssues and PRs related to the SQLite subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sqlite: reentrancy into a running statement from a user-defined function is unguarded

5 participants

@TrevorBurnham@nodejs-github-bot@trivikr@avivkeller@panva
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

sqlite: reject reentry into a running statement - #65106

Closed
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry
Closed

sqlite: reject reentry into a running statement#65106
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes: #65102

SQLite forbids stepping, resetting, or finalizing a statement while that statement's own user-defined function callback is on the stack. The callback depth added in 5cef767 (#64743) is tracked per database, so it cannot distinguish reentry into the running statement from the common pattern of querying a different statement from a callback.

This PR adds a per-statement flag, set for the duration of an execution, and rejects step/reset/finalize on that statement with ERR_INVALID_STATE: statement is currently being executed.

Severity

Reentry corrupts the running virtual machine. Resetting from a callback halts a VM whose sqlite3_step() frame is still live:

// SIGSEGV before this changeletstmt;letfirst=true;db.function('f',()=>{if(first){first=false;stmt.get();}return1;});stmt=db.prepare('SELECT f(), x FROM t');stmt.all();

Verified on v24.15.0 and current main. The crash needs the reentry to be bounded — the repros in #65102 recurse without limit, so V8's stack overflows before control returns into the corrupted step(), which is why that issue concluded it was correctness-only.

reentrant call from callbackbefore
get() / run() / all() (each resets)SIGSEGV
iterator.return() (bare sqlite3_reset)SIGSEGV
aggregate step / result callbackabort, CHECK(agg->initialized) in Release
iterator.next() (steps, no reset)silently consumed rows from the iteration in progress
recursive get(), unboundedMaximum call stack size exceeded
columns() / sourceSQL / expandedSQLunaffected

The aggregate row is a distinct corruption path: the reentrant reset tears down aggregate state mid-xValue, tripping the CHECK in CustomAggregate::DestroyAggregateData. That CHECK is compiled into release builds, so it aborts in production rather than only under a debug build.

Covered entry points: all(), get(), run(), iterate(), iterator.next(), iterator.return(), close(), [Symbol.dispose](), and the four SQL tag store methods. close() and [Symbol.dispose]() are included because finalizing mid-step frees the virtual machine that sqlite3_step() is still executing.

Prior art: #63183 took this approach alongside its own database-level guard. That PR was closed once #64743 landed, so this salvages the per-statement half and builds on the guard already in main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/sqlite

@nodejs-github-botnodejs-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 Aug 7, 2026
@codecov

codecovBot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.53846% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.32%. Comparing base (e2d7b34) to head (abab573).
⚠️ Report is 62 commits behind head on main.

Files with missing linesPatch %Lines
src/node_sqlite.cc54.54%4 Missing and 6 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65106 +/- ##
========================================
Coverage 90.31% 90.32% ========================================
Files 759 760 +1 Lines 248290 248551 +261 Branches 46859 46918 +59 ========================================
+ Hits 224241 224501 +260 + Misses 15472 15460 -12 - Partials 8577 8590 +13 
Files with missing linesCoverage Δ
src/node_sqlite.h83.56% <100.00%> (+0.95%)⬆️
src/node_sqlite.cc81.24% <54.54%> (+0.18%)⬆️

... and 69 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.

@trivikr

This comment was marked as outdated.

@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from ee4e9b5 to 986eb0eCompareAugust 7, 2026 17:41
@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

@trivikr I've addressed the issues you noted. The format-cpp check is now timing out after 15 minutes ("The operation was canceled"); the same check succeeds locally.

Comment threadsrc/node_sqlite.cc
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 986eb0e to fde8f11CompareAugust 8, 2026 12:09
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js Outdated
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from fde8f11 to 02e92f5CompareAugust 8, 2026 14:58
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 9, 2026
@trivikrtrivikr added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 9, 2026
@avivkelleravivkeller added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 9, 2026
@avivkeller

Copy link
Copy Markdown
Member
Commit Queue failed
- Loading data for nodejs/node/pull/65106
✔ Done loading data for nodejs/node/pull/65106
----------------------------------- PR info ------------------------------------
Title sqlite: reject reentry into a running statement (#65106)
Author Trevor Burnham <trevorburnham@gmail.com> (@TrevorBurnham)
Branch TrevorBurnham:sqlite/guard-statement-reentry -> nodejs:main
Labels c++, author ready, needs-ci, commit-queue, sqlite
Commits 1
- sqlite: reject reentry into a running statement
Committers 1
- Trevor Burnham <trevorburnham@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Fri, 07 Aug 2026 14:58:01 GMT
✔ Approvals: 1
✔ - Trivikram Kamat (@trivikr): https://github.com/nodejs/node/pull/65106#pullrequestreview-4889248717
✘ This PR needs to wait 113 more hours to land (or 0 minutes if there is one more approval)
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-08T17:08:25Z: https://ci.nodejs.org/job/node-test-pull-request/75659/
- Querying data for job/node-test-pull-request/75659/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /Users/avivkeller/Documents/projects/nodejs/node/.ncu
/nodejs/node/actions/runs/

@avivkelleravivkeller added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 9, 2026
SQLite forbids stepping, resetting, or finalizing a statement while
that statement's own user-defined function callback is on the stack.
The callback depth added in 5cef767 is tracked per database, so
it cannot tell reentry into the running statement apart from the
common pattern of querying a different statement from a callback.
Mark the statement being executed and reject step, reset, and
finalize on that statement with ERR_INVALID_STATE. Statements other
than the running one are unaffected.
Reentry corrupts the running virtual machine rather than merely
producing a wrong answer. Resetting from a callback halts a VM whose
sqlite3_step() frame is still live, so a bounded reentrant get() or
run(), and iterator.return() with its bare sqlite3_reset(), all
segfault; from an aggregate's step or result callback the same reset
tears down aggregate state mid-xValue and aborts on a CHECK in a
release build. Where it does not crash it is still wrong: a
reentrant iterator.next() silently consumed rows from the iteration
in progress, and a recursive get() surfaced a V8 stack overflow
instead of the constraint. close() and [Symbol.dispose]() are
covered too, since finalizing mid-step frees the running VM.
The mark is set before parameters are bound, so a getter or valueOf()
that reenters while its own arguments are being evaluated is rejected
as well. Without this, two iterators could share one virtual machine
and interleave rows from a single result set.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Assisted-by: claude:opus-5
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 02e92f5 to abab573CompareAugust 10, 2026 13:36
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@nodejs-github-bot

This comment was marked as resolved.

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

^ Updated the PR to cover sqlite3_reset. The PR description has been updated as well.

@panvapanva removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 10, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

This PR was largely superseded by #65156. Closing it out. I've spun off one small follow-up PR: #65294

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

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.needs-ciPRs that need a full CI run.sqliteIssues and PRs related to the SQLite subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sqlite: reentrancy into a running statement from a user-defined function is unguarded

5 participants

@TrevorBurnham@nodejs-github-bot@trivikr@avivkeller@panva
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

sqlite: reject reentry into a running statement - #65106

Closed
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry
Closed

sqlite: reject reentry into a running statement#65106
TrevorBurnham wants to merge 1 commit into
nodejs:mainfrom
TrevorBurnham:sqlite/guard-statement-reentry

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes: #65102

SQLite forbids stepping, resetting, or finalizing a statement while that statement's own user-defined function callback is on the stack. The callback depth added in 5cef767 (#64743) is tracked per database, so it cannot distinguish reentry into the running statement from the common pattern of querying a different statement from a callback.

This PR adds a per-statement flag, set for the duration of an execution, and rejects step/reset/finalize on that statement with ERR_INVALID_STATE: statement is currently being executed.

Severity

Reentry corrupts the running virtual machine. Resetting from a callback halts a VM whose sqlite3_step() frame is still live:

// SIGSEGV before this changeletstmt;letfirst=true;db.function('f',()=>{if(first){first=false;stmt.get();}return1;});stmt=db.prepare('SELECT f(), x FROM t');stmt.all();

Verified on v24.15.0 and current main. The crash needs the reentry to be bounded — the repros in #65102 recurse without limit, so V8's stack overflows before control returns into the corrupted step(), which is why that issue concluded it was correctness-only.

reentrant call from callbackbefore
get() / run() / all() (each resets)SIGSEGV
iterator.return() (bare sqlite3_reset)SIGSEGV
aggregate step / result callbackabort, CHECK(agg->initialized) in Release
iterator.next() (steps, no reset)silently consumed rows from the iteration in progress
recursive get(), unboundedMaximum call stack size exceeded
columns() / sourceSQL / expandedSQLunaffected

The aggregate row is a distinct corruption path: the reentrant reset tears down aggregate state mid-xValue, tripping the CHECK in CustomAggregate::DestroyAggregateData. That CHECK is compiled into release builds, so it aborts in production rather than only under a debug build.

Covered entry points: all(), get(), run(), iterate(), iterator.next(), iterator.return(), close(), [Symbol.dispose](), and the four SQL tag store methods. close() and [Symbol.dispose]() are included because finalizing mid-step frees the virtual machine that sqlite3_step() is still executing.

Prior art: #63183 took this approach alongside its own database-level guard. That PR was closed once #64743 landed, so this salvages the per-statement half and builds on the guard already in main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/sqlite

@nodejs-github-botnodejs-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 Aug 7, 2026
@codecov

codecovBot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.53846% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.32%. Comparing base (e2d7b34) to head (abab573).
⚠️ Report is 62 commits behind head on main.

Files with missing linesPatch %Lines
src/node_sqlite.cc54.54%4 Missing and 6 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65106 +/- ##
========================================
Coverage 90.31% 90.32% ========================================
Files 759 760 +1 Lines 248290 248551 +261 Branches 46859 46918 +59 ========================================
+ Hits 224241 224501 +260 + Misses 15472 15460 -12 - Partials 8577 8590 +13 
Files with missing linesCoverage Δ
src/node_sqlite.h83.56% <100.00%> (+0.95%)⬆️
src/node_sqlite.cc81.24% <54.54%> (+0.18%)⬆️

... and 69 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.

@trivikr

This comment was marked as outdated.

@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from ee4e9b5 to 986eb0eCompareAugust 7, 2026 17:41
@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

@trivikr I've addressed the issues you noted. The format-cpp check is now timing out after 15 minutes ("The operation was canceled"); the same check succeeds locally.

Comment threadsrc/node_sqlite.cc
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 986eb0e to fde8f11CompareAugust 8, 2026 12:09
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js
Comment threadtest/parallel/test-sqlite-udf-statement-reentry.js Outdated
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from fde8f11 to 02e92f5CompareAugust 8, 2026 14:58
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 8, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 9, 2026
@trivikrtrivikr added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 9, 2026
@avivkelleravivkeller added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 9, 2026
@avivkeller

Copy link
Copy Markdown
Member
Commit Queue failed
- Loading data for nodejs/node/pull/65106
✔ Done loading data for nodejs/node/pull/65106
----------------------------------- PR info ------------------------------------
Title sqlite: reject reentry into a running statement (#65106)
Author Trevor Burnham <trevorburnham@gmail.com> (@TrevorBurnham)
Branch TrevorBurnham:sqlite/guard-statement-reentry -> nodejs:main
Labels c++, author ready, needs-ci, commit-queue, sqlite
Commits 1
- sqlite: reject reentry into a running statement
Committers 1
- Trevor Burnham <trevorburnham@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65106
Fixes: https://github.com/nodejs/node/issues/65102
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Fri, 07 Aug 2026 14:58:01 GMT
✔ Approvals: 1
✔ - Trivikram Kamat (@trivikr): https://github.com/nodejs/node/pull/65106#pullrequestreview-4889248717
✘ This PR needs to wait 113 more hours to land (or 0 minutes if there is one more approval)
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-08T17:08:25Z: https://ci.nodejs.org/job/node-test-pull-request/75659/
- Querying data for job/node-test-pull-request/75659/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ Aborted `git node land` session in /Users/avivkeller/Documents/projects/nodejs/node/.ncu
/nodejs/node/actions/runs/

@avivkelleravivkeller added commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Aug 9, 2026
SQLite forbids stepping, resetting, or finalizing a statement while
that statement's own user-defined function callback is on the stack.
The callback depth added in 5cef767 is tracked per database, so
it cannot tell reentry into the running statement apart from the
common pattern of querying a different statement from a callback.
Mark the statement being executed and reject step, reset, and
finalize on that statement with ERR_INVALID_STATE. Statements other
than the running one are unaffected.
Reentry corrupts the running virtual machine rather than merely
producing a wrong answer. Resetting from a callback halts a VM whose
sqlite3_step() frame is still live, so a bounded reentrant get() or
run(), and iterator.return() with its bare sqlite3_reset(), all
segfault; from an aggregate's step or result callback the same reset
tears down aggregate state mid-xValue and aborts on a CHECK in a
release build. Where it does not crash it is still wrong: a
reentrant iterator.next() silently consumed rows from the iteration
in progress, and a recursive get() surfaced a V8 stack overflow
instead of the constraint. close() and [Symbol.dispose]() are
covered too, since finalizing mid-step frees the running VM.
The mark is set before parameters are bound, so a getter or valueOf()
that reenters while its own arguments are being evaluated is rejected
as well. Without this, two iterators could share one virtual machine
and interleave rows from a single result set.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Assisted-by: claude:opus-5
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite/guard-statement-reentry branch from 02e92f5 to abab573CompareAugust 10, 2026 13:36
@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 10, 2026
@nodejs-github-bot

This comment was marked as resolved.

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

^ Updated the PR to cover sqlite3_reset. The PR description has been updated as well.

@panvapanva removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 10, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@TrevorBurnham

Copy link
Copy Markdown
ContributorAuthor

This PR was largely superseded by #65156. Closing it out. I've spun off one small follow-up PR: #65294

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

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.needs-ciPRs that need a full CI run.sqliteIssues and PRs related to the SQLite subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sqlite: reentrancy into a running statement from a user-defined function is unguarded

5 participants

@TrevorBurnham@nodejs-github-bot@trivikr@avivkeller@panva