sqlite: reject closing a session from a callback - #65454

Closed
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback
Closed

sqlite: reject closing a session from a callback#65454
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

session.close() from a SQLite callback frees the session while SQLite is still using it:

constdb=newDatabaseSync(':memory:');db.exec('CREATE TABLE t(a PRIMARY KEY)');constsession=db.createSession();db.setAuthorizer(()=>(session.close(),constants.SQLITE_OK));db.exec('INSERT INTO t VALUES (1)');// segfault

SQLite's pre-update hook walks the connection's session list and calls sessionTableInfo(), which prepares and steps PRAGMA table_xinfo. That reaches JavaScript, so a callback runs while the walk still holds pointers into the session it is visiting; sqlite3session_delete() there frees them.

Two callbacks reach that window: an authorizer callback (the reported case) and a 'sqlite.db.query' subscriber, which fires on SQLITE_TRACE_PROFILE when the internal PRAGMA finishes. Both segfault on main, so an authorizer-only guard would be incomplete.

The guard rejects close() and Symbol.dispose whenever the connection is in any callback, matching the existing db.close() rule. Node cannot tell whether SQLite is currently inside xPreUpdate, so a narrower guard would rest on no other callback ever running there. It does newly reject closing a session from a user-defined function, which is safe today. Disposal stays a no-op for an already-closed session; only a live one throws.

Rebased onto #65449, which made session[Symbol.dispose]() throw for a session that is generating a changeset. This guard follows that precedent: the callback check sits below the in-use check, so the more specific message still wins, and #65449's test covers both orders. One consequence is that a using declaration inside a callback demotes the block's own error to SuppressedError; test-sqlite-session.js pins that.

Fixes: #65428

@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 21, 2026
@TrevorBurnham
TrevorBurnham marked this pull request as ready for review August 21, 2026 17:01
@codecov

codecovBot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… Project coverage is 90.06%. Comparing base (0544741) to head (2e65c4c).
⚠️ Report is 30 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65454 +/- ##
==========================================
- Coverage 90.07% 90.06% -0.02% 
==========================================
Files 751 751 Lines 254921 254923 +2 Branches 48129 48124 -5 ==========================================
- Hits 229627 229602 -25 - Misses 16479 16504 +25 - Partials 8815 8817 +2 
Files with missing linesCoverage Ξ”
src/node_sqlite.cc82.12% <100.00%> (+0.01%)⬆️

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

@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@nodejs-github-bot

This comment was marked as outdated.

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 28, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: nodejs#65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Pin the deliberate over-rejection with a test: closing from a
user-defined function is safe today but rejected anyway, because Node
cannot tell whether SQLite is inside its pre-update hook.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Cover the `using` form of the rejected disposal, whose block error is
demoted to SuppressedError, and note why the callback check has to stay
below the changeset check in Session::Close().
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite-session-close-in-callback branch from 5037bc8 to 2e65c4cCompareAugust 28, 2026 14:48
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

trivikr pushed a commit that referenced this pull request Aug 30, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
@trivikr

Copy link
Copy Markdown
Member

Landed in 01c1300

@trivikrtrivikr closed this Aug 30, 2026
@trivikr

Copy link
Copy Markdown
Member

Landed manually, since the CI failures in test-module-builtin-experimental are unrelated to this PR and were fixed in #65636.

aduh95 pushed a commit that referenced this pull request Sep 3, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
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: two reentrancy guard gaps let a callback free the object SQLite is still using

3 participants

@TrevorBurnham@nodejs-github-bot@trivikr
, '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 closing a session from a callback - #65454

Closed
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback
Closed

sqlite: reject closing a session from a callback#65454
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

session.close() from a SQLite callback frees the session while SQLite is still using it:

constdb=newDatabaseSync(':memory:');db.exec('CREATE TABLE t(a PRIMARY KEY)');constsession=db.createSession();db.setAuthorizer(()=>(session.close(),constants.SQLITE_OK));db.exec('INSERT INTO t VALUES (1)');// segfault

SQLite's pre-update hook walks the connection's session list and calls sessionTableInfo(), which prepares and steps PRAGMA table_xinfo. That reaches JavaScript, so a callback runs while the walk still holds pointers into the session it is visiting; sqlite3session_delete() there frees them.

Two callbacks reach that window: an authorizer callback (the reported case) and a 'sqlite.db.query' subscriber, which fires on SQLITE_TRACE_PROFILE when the internal PRAGMA finishes. Both segfault on main, so an authorizer-only guard would be incomplete.

The guard rejects close() and Symbol.dispose whenever the connection is in any callback, matching the existing db.close() rule. Node cannot tell whether SQLite is currently inside xPreUpdate, so a narrower guard would rest on no other callback ever running there. It does newly reject closing a session from a user-defined function, which is safe today. Disposal stays a no-op for an already-closed session; only a live one throws.

Rebased onto #65449, which made session[Symbol.dispose]() throw for a session that is generating a changeset. This guard follows that precedent: the callback check sits below the in-use check, so the more specific message still wins, and #65449's test covers both orders. One consequence is that a using declaration inside a callback demotes the block's own error to SuppressedError; test-sqlite-session.js pins that.

Fixes: #65428

@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 21, 2026
@TrevorBurnham
TrevorBurnham marked this pull request as ready for review August 21, 2026 17:01
@codecov

codecovBot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… Project coverage is 90.06%. Comparing base (0544741) to head (2e65c4c).
⚠️ Report is 30 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65454 +/- ##
==========================================
- Coverage 90.07% 90.06% -0.02% 
==========================================
Files 751 751 Lines 254921 254923 +2 Branches 48129 48124 -5 ==========================================
- Hits 229627 229602 -25 - Misses 16479 16504 +25 - Partials 8815 8817 +2 
Files with missing linesCoverage Ξ”
src/node_sqlite.cc82.12% <100.00%> (+0.01%)⬆️

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

@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@nodejs-github-bot

This comment was marked as outdated.

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 28, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: nodejs#65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Pin the deliberate over-rejection with a test: closing from a
user-defined function is safe today but rejected anyway, because Node
cannot tell whether SQLite is inside its pre-update hook.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Cover the `using` form of the rejected disposal, whose block error is
demoted to SuppressedError, and note why the callback check has to stay
below the changeset check in Session::Close().
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite-session-close-in-callback branch from 5037bc8 to 2e65c4cCompareAugust 28, 2026 14:48
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

trivikr pushed a commit that referenced this pull request Aug 30, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
@trivikr

Copy link
Copy Markdown
Member

Landed in 01c1300

@trivikrtrivikr closed this Aug 30, 2026
@trivikr

Copy link
Copy Markdown
Member

Landed manually, since the CI failures in test-module-builtin-experimental are unrelated to this PR and were fixed in #65636.

aduh95 pushed a commit that referenced this pull request Sep 3, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
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: two reentrancy guard gaps let a callback free the object SQLite is still using

3 participants

@TrevorBurnham@nodejs-github-bot@trivikr
, '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 closing a session from a callback - #65454

Closed
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback
Closed

sqlite: reject closing a session from a callback#65454
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

session.close() from a SQLite callback frees the session while SQLite is still using it:

constdb=newDatabaseSync(':memory:');db.exec('CREATE TABLE t(a PRIMARY KEY)');constsession=db.createSession();db.setAuthorizer(()=>(session.close(),constants.SQLITE_OK));db.exec('INSERT INTO t VALUES (1)');// segfault

SQLite's pre-update hook walks the connection's session list and calls sessionTableInfo(), which prepares and steps PRAGMA table_xinfo. That reaches JavaScript, so a callback runs while the walk still holds pointers into the session it is visiting; sqlite3session_delete() there frees them.

Two callbacks reach that window: an authorizer callback (the reported case) and a 'sqlite.db.query' subscriber, which fires on SQLITE_TRACE_PROFILE when the internal PRAGMA finishes. Both segfault on main, so an authorizer-only guard would be incomplete.

The guard rejects close() and Symbol.dispose whenever the connection is in any callback, matching the existing db.close() rule. Node cannot tell whether SQLite is currently inside xPreUpdate, so a narrower guard would rest on no other callback ever running there. It does newly reject closing a session from a user-defined function, which is safe today. Disposal stays a no-op for an already-closed session; only a live one throws.

Rebased onto #65449, which made session[Symbol.dispose]() throw for a session that is generating a changeset. This guard follows that precedent: the callback check sits below the in-use check, so the more specific message still wins, and #65449's test covers both orders. One consequence is that a using declaration inside a callback demotes the block's own error to SuppressedError; test-sqlite-session.js pins that.

Fixes: #65428

@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 21, 2026
@TrevorBurnham
TrevorBurnham marked this pull request as ready for review August 21, 2026 17:01
@codecov

codecovBot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… Project coverage is 90.06%. Comparing base (0544741) to head (2e65c4c).
⚠️ Report is 30 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65454 +/- ##
==========================================
- Coverage 90.07% 90.06% -0.02% 
==========================================
Files 751 751 Lines 254921 254923 +2 Branches 48129 48124 -5 ==========================================
- Hits 229627 229602 -25 - Misses 16479 16504 +25 - Partials 8815 8817 +2 
Files with missing linesCoverage Ξ”
src/node_sqlite.cc82.12% <100.00%> (+0.01%)⬆️

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

@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@nodejs-github-bot

This comment was marked as outdated.

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 28, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: nodejs#65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Pin the deliberate over-rejection with a test: closing from a
user-defined function is safe today but rejected anyway, because Node
cannot tell whether SQLite is inside its pre-update hook.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Cover the `using` form of the rejected disposal, whose block error is
demoted to SuppressedError, and note why the callback check has to stay
below the changeset check in Session::Close().
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite-session-close-in-callback branch from 5037bc8 to 2e65c4cCompareAugust 28, 2026 14:48
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

trivikr pushed a commit that referenced this pull request Aug 30, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
@trivikr

Copy link
Copy Markdown
Member

Landed in 01c1300

@trivikrtrivikr closed this Aug 30, 2026
@trivikr

Copy link
Copy Markdown
Member

Landed manually, since the CI failures in test-module-builtin-experimental are unrelated to this PR and were fixed in #65636.

aduh95 pushed a commit that referenced this pull request Sep 3, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
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: two reentrancy guard gaps let a callback free the object SQLite is still using

3 participants

@TrevorBurnham@nodejs-github-bot@trivikr
, '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 closing a session from a callback - #65454

Closed
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback
Closed

sqlite: reject closing a session from a callback#65454
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

session.close() from a SQLite callback frees the session while SQLite is still using it:

constdb=newDatabaseSync(':memory:');db.exec('CREATE TABLE t(a PRIMARY KEY)');constsession=db.createSession();db.setAuthorizer(()=>(session.close(),constants.SQLITE_OK));db.exec('INSERT INTO t VALUES (1)');// segfault

SQLite's pre-update hook walks the connection's session list and calls sessionTableInfo(), which prepares and steps PRAGMA table_xinfo. That reaches JavaScript, so a callback runs while the walk still holds pointers into the session it is visiting; sqlite3session_delete() there frees them.

Two callbacks reach that window: an authorizer callback (the reported case) and a 'sqlite.db.query' subscriber, which fires on SQLITE_TRACE_PROFILE when the internal PRAGMA finishes. Both segfault on main, so an authorizer-only guard would be incomplete.

The guard rejects close() and Symbol.dispose whenever the connection is in any callback, matching the existing db.close() rule. Node cannot tell whether SQLite is currently inside xPreUpdate, so a narrower guard would rest on no other callback ever running there. It does newly reject closing a session from a user-defined function, which is safe today. Disposal stays a no-op for an already-closed session; only a live one throws.

Rebased onto #65449, which made session[Symbol.dispose]() throw for a session that is generating a changeset. This guard follows that precedent: the callback check sits below the in-use check, so the more specific message still wins, and #65449's test covers both orders. One consequence is that a using declaration inside a callback demotes the block's own error to SuppressedError; test-sqlite-session.js pins that.

Fixes: #65428

@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 21, 2026
@TrevorBurnham
TrevorBurnham marked this pull request as ready for review August 21, 2026 17:01
@codecov

codecovBot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… Project coverage is 90.06%. Comparing base (0544741) to head (2e65c4c).
⚠️ Report is 30 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65454 +/- ##
==========================================
- Coverage 90.07% 90.06% -0.02% 
==========================================
Files 751 751 Lines 254921 254923 +2 Branches 48129 48124 -5 ==========================================
- Hits 229627 229602 -25 - Misses 16479 16504 +25 - Partials 8815 8817 +2 
Files with missing linesCoverage Ξ”
src/node_sqlite.cc82.12% <100.00%> (+0.01%)⬆️

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

@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@nodejs-github-bot

This comment was marked as outdated.

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 28, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: nodejs#65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Pin the deliberate over-rejection with a test: closing from a
user-defined function is safe today but rejected anyway, because Node
cannot tell whether SQLite is inside its pre-update hook.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Cover the `using` form of the rejected disposal, whose block error is
demoted to SuppressedError, and note why the callback check has to stay
below the changeset check in Session::Close().
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite-session-close-in-callback branch from 5037bc8 to 2e65c4cCompareAugust 28, 2026 14:48
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

trivikr pushed a commit that referenced this pull request Aug 30, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
@trivikr

Copy link
Copy Markdown
Member

Landed in 01c1300

@trivikrtrivikr closed this Aug 30, 2026
@trivikr

Copy link
Copy Markdown
Member

Landed manually, since the CI failures in test-module-builtin-experimental are unrelated to this PR and were fixed in #65636.

aduh95 pushed a commit that referenced this pull request Sep 3, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
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: two reentrancy guard gaps let a callback free the object SQLite is still using

3 participants

@TrevorBurnham@nodejs-github-bot@trivikr
, '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 closing a session from a callback - #65454

Closed
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback
Closed

sqlite: reject closing a session from a callback#65454
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

session.close() from a SQLite callback frees the session while SQLite is still using it:

constdb=newDatabaseSync(':memory:');db.exec('CREATE TABLE t(a PRIMARY KEY)');constsession=db.createSession();db.setAuthorizer(()=>(session.close(),constants.SQLITE_OK));db.exec('INSERT INTO t VALUES (1)');// segfault

SQLite's pre-update hook walks the connection's session list and calls sessionTableInfo(), which prepares and steps PRAGMA table_xinfo. That reaches JavaScript, so a callback runs while the walk still holds pointers into the session it is visiting; sqlite3session_delete() there frees them.

Two callbacks reach that window: an authorizer callback (the reported case) and a 'sqlite.db.query' subscriber, which fires on SQLITE_TRACE_PROFILE when the internal PRAGMA finishes. Both segfault on main, so an authorizer-only guard would be incomplete.

The guard rejects close() and Symbol.dispose whenever the connection is in any callback, matching the existing db.close() rule. Node cannot tell whether SQLite is currently inside xPreUpdate, so a narrower guard would rest on no other callback ever running there. It does newly reject closing a session from a user-defined function, which is safe today. Disposal stays a no-op for an already-closed session; only a live one throws.

Rebased onto #65449, which made session[Symbol.dispose]() throw for a session that is generating a changeset. This guard follows that precedent: the callback check sits below the in-use check, so the more specific message still wins, and #65449's test covers both orders. One consequence is that a using declaration inside a callback demotes the block's own error to SuppressedError; test-sqlite-session.js pins that.

Fixes: #65428

@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 21, 2026
@TrevorBurnham
TrevorBurnham marked this pull request as ready for review August 21, 2026 17:01
@codecov

codecovBot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… Project coverage is 90.06%. Comparing base (0544741) to head (2e65c4c).
⚠️ Report is 30 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65454 +/- ##
==========================================
- Coverage 90.07% 90.06% -0.02% 
==========================================
Files 751 751 Lines 254921 254923 +2 Branches 48129 48124 -5 ==========================================
- Hits 229627 229602 -25 - Misses 16479 16504 +25 - Partials 8815 8817 +2 
Files with missing linesCoverage Ξ”
src/node_sqlite.cc82.12% <100.00%> (+0.01%)⬆️

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

@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@nodejs-github-bot

This comment was marked as outdated.

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 28, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: nodejs#65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Pin the deliberate over-rejection with a test: closing from a
user-defined function is safe today but rejected anyway, because Node
cannot tell whether SQLite is inside its pre-update hook.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Cover the `using` form of the rejected disposal, whose block error is
demoted to SuppressedError, and note why the callback check has to stay
below the changeset check in Session::Close().
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite-session-close-in-callback branch from 5037bc8 to 2e65c4cCompareAugust 28, 2026 14:48
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

trivikr pushed a commit that referenced this pull request Aug 30, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
@trivikr

Copy link
Copy Markdown
Member

Landed in 01c1300

@trivikrtrivikr closed this Aug 30, 2026
@trivikr

Copy link
Copy Markdown
Member

Landed manually, since the CI failures in test-module-builtin-experimental are unrelated to this PR and were fixed in #65636.

aduh95 pushed a commit that referenced this pull request Sep 3, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
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: two reentrancy guard gaps let a callback free the object SQLite is still using

3 participants

@TrevorBurnham@nodejs-github-bot@trivikr
, '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 closing a session from a callback - #65454

Closed
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback
Closed

sqlite: reject closing a session from a callback#65454
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

session.close() from a SQLite callback frees the session while SQLite is still using it:

constdb=newDatabaseSync(':memory:');db.exec('CREATE TABLE t(a PRIMARY KEY)');constsession=db.createSession();db.setAuthorizer(()=>(session.close(),constants.SQLITE_OK));db.exec('INSERT INTO t VALUES (1)');// segfault

SQLite's pre-update hook walks the connection's session list and calls sessionTableInfo(), which prepares and steps PRAGMA table_xinfo. That reaches JavaScript, so a callback runs while the walk still holds pointers into the session it is visiting; sqlite3session_delete() there frees them.

Two callbacks reach that window: an authorizer callback (the reported case) and a 'sqlite.db.query' subscriber, which fires on SQLITE_TRACE_PROFILE when the internal PRAGMA finishes. Both segfault on main, so an authorizer-only guard would be incomplete.

The guard rejects close() and Symbol.dispose whenever the connection is in any callback, matching the existing db.close() rule. Node cannot tell whether SQLite is currently inside xPreUpdate, so a narrower guard would rest on no other callback ever running there. It does newly reject closing a session from a user-defined function, which is safe today. Disposal stays a no-op for an already-closed session; only a live one throws.

Rebased onto #65449, which made session[Symbol.dispose]() throw for a session that is generating a changeset. This guard follows that precedent: the callback check sits below the in-use check, so the more specific message still wins, and #65449's test covers both orders. One consequence is that a using declaration inside a callback demotes the block's own error to SuppressedError; test-sqlite-session.js pins that.

Fixes: #65428

@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 21, 2026
@TrevorBurnham
TrevorBurnham marked this pull request as ready for review August 21, 2026 17:01
@codecov

codecovBot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… Project coverage is 90.06%. Comparing base (0544741) to head (2e65c4c).
⚠️ Report is 30 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65454 +/- ##
==========================================
- Coverage 90.07% 90.06% -0.02% 
==========================================
Files 751 751 Lines 254921 254923 +2 Branches 48129 48124 -5 ==========================================
- Hits 229627 229602 -25 - Misses 16479 16504 +25 - Partials 8815 8817 +2 
Files with missing linesCoverage Ξ”
src/node_sqlite.cc82.12% <100.00%> (+0.01%)⬆️

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

@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@nodejs-github-bot

This comment was marked as outdated.

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 28, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: nodejs#65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Pin the deliberate over-rejection with a test: closing from a
user-defined function is safe today but rejected anyway, because Node
cannot tell whether SQLite is inside its pre-update hook.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Cover the `using` form of the rejected disposal, whose block error is
demoted to SuppressedError, and note why the callback check has to stay
below the changeset check in Session::Close().
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite-session-close-in-callback branch from 5037bc8 to 2e65c4cCompareAugust 28, 2026 14:48
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

trivikr pushed a commit that referenced this pull request Aug 30, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
@trivikr

Copy link
Copy Markdown
Member

Landed in 01c1300

@trivikrtrivikr closed this Aug 30, 2026
@trivikr

Copy link
Copy Markdown
Member

Landed manually, since the CI failures in test-module-builtin-experimental are unrelated to this PR and were fixed in #65636.

aduh95 pushed a commit that referenced this pull request Sep 3, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
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: two reentrancy guard gaps let a callback free the object SQLite is still using

3 participants

@TrevorBurnham@nodejs-github-bot@trivikr
, '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 closing a session from a callback - #65454

Closed
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback
Closed

sqlite: reject closing a session from a callback#65454
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

session.close() from a SQLite callback frees the session while SQLite is still using it:

constdb=newDatabaseSync(':memory:');db.exec('CREATE TABLE t(a PRIMARY KEY)');constsession=db.createSession();db.setAuthorizer(()=>(session.close(),constants.SQLITE_OK));db.exec('INSERT INTO t VALUES (1)');// segfault

SQLite's pre-update hook walks the connection's session list and calls sessionTableInfo(), which prepares and steps PRAGMA table_xinfo. That reaches JavaScript, so a callback runs while the walk still holds pointers into the session it is visiting; sqlite3session_delete() there frees them.

Two callbacks reach that window: an authorizer callback (the reported case) and a 'sqlite.db.query' subscriber, which fires on SQLITE_TRACE_PROFILE when the internal PRAGMA finishes. Both segfault on main, so an authorizer-only guard would be incomplete.

The guard rejects close() and Symbol.dispose whenever the connection is in any callback, matching the existing db.close() rule. Node cannot tell whether SQLite is currently inside xPreUpdate, so a narrower guard would rest on no other callback ever running there. It does newly reject closing a session from a user-defined function, which is safe today. Disposal stays a no-op for an already-closed session; only a live one throws.

Rebased onto #65449, which made session[Symbol.dispose]() throw for a session that is generating a changeset. This guard follows that precedent: the callback check sits below the in-use check, so the more specific message still wins, and #65449's test covers both orders. One consequence is that a using declaration inside a callback demotes the block's own error to SuppressedError; test-sqlite-session.js pins that.

Fixes: #65428

@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 21, 2026
@TrevorBurnham
TrevorBurnham marked this pull request as ready for review August 21, 2026 17:01
@codecov

codecovBot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… Project coverage is 90.06%. Comparing base (0544741) to head (2e65c4c).
⚠️ Report is 30 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65454 +/- ##
==========================================
- Coverage 90.07% 90.06% -0.02% 
==========================================
Files 751 751 Lines 254921 254923 +2 Branches 48129 48124 -5 ==========================================
- Hits 229627 229602 -25 - Misses 16479 16504 +25 - Partials 8815 8817 +2 
Files with missing linesCoverage Ξ”
src/node_sqlite.cc82.12% <100.00%> (+0.01%)⬆️

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

@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@nodejs-github-bot

This comment was marked as outdated.

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 28, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: nodejs#65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Pin the deliberate over-rejection with a test: closing from a
user-defined function is safe today but rejected anyway, because Node
cannot tell whether SQLite is inside its pre-update hook.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Cover the `using` form of the rejected disposal, whose block error is
demoted to SuppressedError, and note why the callback check has to stay
below the changeset check in Session::Close().
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite-session-close-in-callback branch from 5037bc8 to 2e65c4cCompareAugust 28, 2026 14:48
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

trivikr pushed a commit that referenced this pull request Aug 30, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
@trivikr

Copy link
Copy Markdown
Member

Landed in 01c1300

@trivikrtrivikr closed this Aug 30, 2026
@trivikr

Copy link
Copy Markdown
Member

Landed manually, since the CI failures in test-module-builtin-experimental are unrelated to this PR and were fixed in #65636.

aduh95 pushed a commit that referenced this pull request Sep 3, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
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: two reentrancy guard gaps let a callback free the object SQLite is still using

3 participants

@TrevorBurnham@nodejs-github-bot@trivikr
, '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 closing a session from a callback - #65454

Closed
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback
Closed

sqlite: reject closing a session from a callback#65454
TrevorBurnham wants to merge 3 commits into
nodejs:mainfrom
TrevorBurnham:sqlite-session-close-in-callback

Conversation

@TrevorBurnham

@TrevorBurnhamTrevorBurnham commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

session.close() from a SQLite callback frees the session while SQLite is still using it:

constdb=newDatabaseSync(':memory:');db.exec('CREATE TABLE t(a PRIMARY KEY)');constsession=db.createSession();db.setAuthorizer(()=>(session.close(),constants.SQLITE_OK));db.exec('INSERT INTO t VALUES (1)');// segfault

SQLite's pre-update hook walks the connection's session list and calls sessionTableInfo(), which prepares and steps PRAGMA table_xinfo. That reaches JavaScript, so a callback runs while the walk still holds pointers into the session it is visiting; sqlite3session_delete() there frees them.

Two callbacks reach that window: an authorizer callback (the reported case) and a 'sqlite.db.query' subscriber, which fires on SQLITE_TRACE_PROFILE when the internal PRAGMA finishes. Both segfault on main, so an authorizer-only guard would be incomplete.

The guard rejects close() and Symbol.dispose whenever the connection is in any callback, matching the existing db.close() rule. Node cannot tell whether SQLite is currently inside xPreUpdate, so a narrower guard would rest on no other callback ever running there. It does newly reject closing a session from a user-defined function, which is safe today. Disposal stays a no-op for an already-closed session; only a live one throws.

Rebased onto #65449, which made session[Symbol.dispose]() throw for a session that is generating a changeset. This guard follows that precedent: the callback check sits below the in-use check, so the more specific message still wins, and #65449's test covers both orders. One consequence is that a using declaration inside a callback demotes the block's own error to SuppressedError; test-sqlite-session.js pins that.

Fixes: #65428

@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 21, 2026
@TrevorBurnham
TrevorBurnham marked this pull request as ready for review August 21, 2026 17:01
@codecov

codecovBot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… Project coverage is 90.06%. Comparing base (0544741) to head (2e65c4c).
⚠️ Report is 30 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65454 +/- ##
==========================================
- Coverage 90.07% 90.06% -0.02% 
==========================================
Files 751 751 Lines 254921 254923 +2 Branches 48129 48124 -5 ==========================================
- Hits 229627 229602 -25 - Misses 16479 16504 +25 - Partials 8815 8817 +2 
Files with missing linesCoverage Ξ”
src/node_sqlite.cc82.12% <100.00%> (+0.01%)⬆️

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

@trivikrtrivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 28, 2026
@nodejs-github-bot

This comment was marked as outdated.

@trivikrtrivikr added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 28, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: nodejs#65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Pin the deliberate over-rejection with a test: closing from a
user-defined function is safe today but rejected anyway, because Node
cannot tell whether SQLite is inside its pre-update hook.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Cover the `using` form of the rejected disposal, whose block error is
demoted to SuppressedError, and note why the callback check has to stay
below the changeset check in Session::Close().
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
@TrevorBurnham
TrevorBurnhamforce-pushed the sqlite-session-close-in-callback branch from 5037bc8 to 2e65c4cCompareAugust 28, 2026 14:48
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

trivikr pushed a commit that referenced this pull request Aug 30, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
@trivikr

Copy link
Copy Markdown
Member

Landed in 01c1300

@trivikrtrivikr closed this Aug 30, 2026
@trivikr

Copy link
Copy Markdown
Member

Landed manually, since the CI failures in test-module-builtin-experimental are unrelated to this PR and were fixed in #65636.

aduh95 pushed a commit that referenced this pull request Sep 3, 2026
SQLite runs "PRAGMA table_xinfo" from inside its pre-update hook, while
it is still walking the connection's session list. Deleting a session
from a callback that the PRAGMA triggers frees memory that walk is still
using. Both an authorizer callback and a 'sqlite.db.query' subscriber
reach that window, and either one segfaults.
Reject session.close() and Symbol.dispose when the connection is inside
any callback. An already-closed session stays a no-op so that disposal
remains idempotent.
Fixes: #65428
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: #65454
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
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: two reentrancy guard gaps let a callback free the object SQLite is still using

3 participants

@TrevorBurnham@nodejs-github-bot@trivikr