Commit fce6754

Browse files
trivikraduh95
authored andcommitted
sqlite: reject busy statement finalization in authorizer
An iterator can leave a statement active between sqlite3_step() calls. Finalizing that statement from an authorizer callback can release its locks and change the outcome of the statement being authorized. Reject close() and Symbol.dispose when the statement is busy and the connection is in an authorizer callback. Continue allowing idle statements to be finalized. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65369Fixes: #65368 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent 29f010f commit fce6754

4 files changed

Lines changed: 53 additions & 8 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,7 @@ Finalizes the prepared statement. An exception is thrown if the statement is
11201120
already finalized. An [`ERR_INVALID_STATE`][] error is thrown if this statement
11211121
is currently executing, which happens when the method is called from a callback
11221122
that the statement itself triggered, such as a user-defined function, an
1123-
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Other statements
1123+
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Idle statements
11241124
on the same connection can be finalized from such a callback. This method is a
11251125
wrapper around [`sqlite3_finalize()`][].
11261126

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,15 @@ inline MaybeLocal<Value> IntegerToValue(Isolate* isolate,
160160
(db)->IsInAuthorizerCallback(), \
161161
"database cannot be accessed from an authorizer callback")
162162

163+
// Finalizing a busy statement from an authorizer can release its locks and
164+
// change the outer statement's outcome.
165+
#defineTHROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt) \
166+
THROW_AND_RETURN_ON_BAD_STATE( \
167+
(env), \
168+
(stmt)->db_->IsInAuthorizerCallback() && \
169+
sqlite3_stmt_busy((stmt)->statement_.get()), \
170+
"database cannot be accessed from an authorizer callback")
171+
163172
// A statement's virtual machine cannot be reentered while sqlite3_step() is
164173
// running it. Finalizing it frees the VM outright, and re-running it resets the
165174
// VM mid-execution; both are use-after-free rather than merely a contract
@@ -2900,6 +2909,7 @@ void StatementSync::Close(const FunctionCallbackInfo<Value>& args) {
29002909
THROW_AND_RETURN_ON_BAD_STATE(
29012910
env, stmt->IsFinalized(), "statement has been finalized");
29022911
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2912+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29032913
stmt->Close();
29042914
}
29052915

@@ -2913,6 +2923,7 @@ void StatementSync::Dispose(const FunctionCallbackInfo<Value>& args) {
29132923
return;
29142924
}
29152925
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2926+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29162927
stmt->Close();
29172928
}
29182929

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,8 @@ class DatabaseSync : public BaseObject {
299299
voidDecrementAuthorizerDepth() { --authorizer_depth_; }
300300
boolIsInAuthorizerCallback() const { return authorizer_depth_ > 0; }
301301

302-
// Finalizing a statement frees its virtual machine, so a callback that
303-
// SQLite invokes from inside sqlite3_step() must not finalize the statement
304-
// being stepped. Other statements on the connection are safe to finalize.
302+
// A callback must not finalize the statement being stepped. Other statements
303+
// are safe unless they are busy during an authorizer callback.
305304
voidPushSteppingStatement(sqlite3_stmt* stmt) {
306305
stepping_statements_.push_back(stmt);
307306
}

β€Žtest/parallel/test-sqlite-authz.jsβ€Ž

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,8 @@ suite('authorizer callback reentrancy', () => {
397397
assert.deepStrictEqual(runInAuthorizer(db,cases),allRejected(cases));
398398
});
399399

400-
// Only the statement being stepped is unsafe to finalize. Other statements
401-
// on the connection have their own virtual machines, so finalizing them from
402-
// a callback is allowed.
403-
it('allows finalizing a statement that is not being executed',()=>{
400+
// An idle statement has no virtual-machine state or locks to release.
401+
it('allows finalizing an idle statement',()=>{
404402
constdb=newDatabaseSync(':memory:');
405403
db.exec('CREATE TABLE t (x INTEGER)');
406404
db.exec('INSERT INTO t VALUES (1)');
@@ -417,6 +415,43 @@ suite('authorizer callback reentrancy', () => {
417415
});
418416
});
419417

418+
// A paused iterator is busy and may hold locks between sqlite3_step() calls.
419+
it('rejects finalizing another active statement',()=>{
420+
for(constmethodof['close','dispose']){
421+
constdb=newDatabaseSync(':memory:');
422+
db.exec('CREATE TABLE t (x INTEGER)');
423+
db.exec('INSERT INTO t VALUES (1), (2), (3)');
424+
conststmt=db.prepare('SELECT x FROM t');
425+
constiter=stmt.iterate();
426+
iter.next();
427+
letoutcome='authorizer callback did not run';
428+
429+
db.setAuthorizer((actionCode)=>{
430+
if(actionCode===constants.SQLITE_DROP_TABLE){
431+
try{
432+
if(method==='close'){
433+
stmt.close();
434+
}else{
435+
stmt[Symbol.dispose]();
436+
}
437+
outcome='did not throw';
438+
}catch(err){
439+
outcome=`${err.code}: ${err.message}`;
440+
}
441+
}
442+
returnconstants.SQLITE_OK;
443+
});
444+
445+
assert.throws(()=>db.exec('DROP TABLE t'),{
446+
code: 'ERR_SQLITE_ERROR',
447+
message: 'database table is locked',
448+
});
449+
assert.strictEqual(outcome,expectedError);
450+
db.setAuthorizer(null);
451+
iter.return();
452+
}
453+
});
454+
420455
// Disposal is idempotent, so a statement that is already finalized must stay
421456
// a no-op even inside a callback. Throwing here would turn a `using` scope's
422457
// real exception into a SuppressedError.

0 commit comments

Comments
Β (0)
, '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

Commit fce6754

Browse files
trivikraduh95
authored andcommitted
sqlite: reject busy statement finalization in authorizer
An iterator can leave a statement active between sqlite3_step() calls. Finalizing that statement from an authorizer callback can release its locks and change the outcome of the statement being authorized. Reject close() and Symbol.dispose when the statement is busy and the connection is in an authorizer callback. Continue allowing idle statements to be finalized. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65369Fixes: #65368 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent 29f010f commit fce6754

4 files changed

Lines changed: 53 additions & 8 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,7 @@ Finalizes the prepared statement. An exception is thrown if the statement is
11201120
already finalized. An [`ERR_INVALID_STATE`][] error is thrown if this statement
11211121
is currently executing, which happens when the method is called from a callback
11221122
that the statement itself triggered, such as a user-defined function, an
1123-
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Other statements
1123+
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Idle statements
11241124
on the same connection can be finalized from such a callback. This method is a
11251125
wrapper around [`sqlite3_finalize()`][].
11261126

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,15 @@ inline MaybeLocal<Value> IntegerToValue(Isolate* isolate,
160160
(db)->IsInAuthorizerCallback(), \
161161
"database cannot be accessed from an authorizer callback")
162162

163+
// Finalizing a busy statement from an authorizer can release its locks and
164+
// change the outer statement's outcome.
165+
#defineTHROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt) \
166+
THROW_AND_RETURN_ON_BAD_STATE( \
167+
(env), \
168+
(stmt)->db_->IsInAuthorizerCallback() && \
169+
sqlite3_stmt_busy((stmt)->statement_.get()), \
170+
"database cannot be accessed from an authorizer callback")
171+
163172
// A statement's virtual machine cannot be reentered while sqlite3_step() is
164173
// running it. Finalizing it frees the VM outright, and re-running it resets the
165174
// VM mid-execution; both are use-after-free rather than merely a contract
@@ -2900,6 +2909,7 @@ void StatementSync::Close(const FunctionCallbackInfo<Value>& args) {
29002909
THROW_AND_RETURN_ON_BAD_STATE(
29012910
env, stmt->IsFinalized(), "statement has been finalized");
29022911
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2912+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29032913
stmt->Close();
29042914
}
29052915

@@ -2913,6 +2923,7 @@ void StatementSync::Dispose(const FunctionCallbackInfo<Value>& args) {
29132923
return;
29142924
}
29152925
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2926+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29162927
stmt->Close();
29172928
}
29182929

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,8 @@ class DatabaseSync : public BaseObject {
299299
voidDecrementAuthorizerDepth() { --authorizer_depth_; }
300300
boolIsInAuthorizerCallback() const { return authorizer_depth_ > 0; }
301301

302-
// Finalizing a statement frees its virtual machine, so a callback that
303-
// SQLite invokes from inside sqlite3_step() must not finalize the statement
304-
// being stepped. Other statements on the connection are safe to finalize.
302+
// A callback must not finalize the statement being stepped. Other statements
303+
// are safe unless they are busy during an authorizer callback.
305304
voidPushSteppingStatement(sqlite3_stmt* stmt) {
306305
stepping_statements_.push_back(stmt);
307306
}

β€Žtest/parallel/test-sqlite-authz.jsβ€Ž

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,8 @@ suite('authorizer callback reentrancy', () => {
397397
assert.deepStrictEqual(runInAuthorizer(db,cases),allRejected(cases));
398398
});
399399

400-
// Only the statement being stepped is unsafe to finalize. Other statements
401-
// on the connection have their own virtual machines, so finalizing them from
402-
// a callback is allowed.
403-
it('allows finalizing a statement that is not being executed',()=>{
400+
// An idle statement has no virtual-machine state or locks to release.
401+
it('allows finalizing an idle statement',()=>{
404402
constdb=newDatabaseSync(':memory:');
405403
db.exec('CREATE TABLE t (x INTEGER)');
406404
db.exec('INSERT INTO t VALUES (1)');
@@ -417,6 +415,43 @@ suite('authorizer callback reentrancy', () => {
417415
});
418416
});
419417

418+
// A paused iterator is busy and may hold locks between sqlite3_step() calls.
419+
it('rejects finalizing another active statement',()=>{
420+
for(constmethodof['close','dispose']){
421+
constdb=newDatabaseSync(':memory:');
422+
db.exec('CREATE TABLE t (x INTEGER)');
423+
db.exec('INSERT INTO t VALUES (1), (2), (3)');
424+
conststmt=db.prepare('SELECT x FROM t');
425+
constiter=stmt.iterate();
426+
iter.next();
427+
letoutcome='authorizer callback did not run';
428+
429+
db.setAuthorizer((actionCode)=>{
430+
if(actionCode===constants.SQLITE_DROP_TABLE){
431+
try{
432+
if(method==='close'){
433+
stmt.close();
434+
}else{
435+
stmt[Symbol.dispose]();
436+
}
437+
outcome='did not throw';
438+
}catch(err){
439+
outcome=`${err.code}: ${err.message}`;
440+
}
441+
}
442+
returnconstants.SQLITE_OK;
443+
});
444+
445+
assert.throws(()=>db.exec('DROP TABLE t'),{
446+
code: 'ERR_SQLITE_ERROR',
447+
message: 'database table is locked',
448+
});
449+
assert.strictEqual(outcome,expectedError);
450+
db.setAuthorizer(null);
451+
iter.return();
452+
}
453+
});
454+
420455
// Disposal is idempotent, so a statement that is already finalized must stay
421456
// a no-op even inside a callback. Throwing here would turn a `using` scope's
422457
// real exception into a SuppressedError.

0 commit comments

Comments
Β (0)
, '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

Commit fce6754

Browse files
trivikraduh95
authored andcommitted
sqlite: reject busy statement finalization in authorizer
An iterator can leave a statement active between sqlite3_step() calls. Finalizing that statement from an authorizer callback can release its locks and change the outcome of the statement being authorized. Reject close() and Symbol.dispose when the statement is busy and the connection is in an authorizer callback. Continue allowing idle statements to be finalized. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65369Fixes: #65368 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent 29f010f commit fce6754

4 files changed

Lines changed: 53 additions & 8 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,7 @@ Finalizes the prepared statement. An exception is thrown if the statement is
11201120
already finalized. An [`ERR_INVALID_STATE`][] error is thrown if this statement
11211121
is currently executing, which happens when the method is called from a callback
11221122
that the statement itself triggered, such as a user-defined function, an
1123-
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Other statements
1123+
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Idle statements
11241124
on the same connection can be finalized from such a callback. This method is a
11251125
wrapper around [`sqlite3_finalize()`][].
11261126

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,15 @@ inline MaybeLocal<Value> IntegerToValue(Isolate* isolate,
160160
(db)->IsInAuthorizerCallback(), \
161161
"database cannot be accessed from an authorizer callback")
162162

163+
// Finalizing a busy statement from an authorizer can release its locks and
164+
// change the outer statement's outcome.
165+
#defineTHROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt) \
166+
THROW_AND_RETURN_ON_BAD_STATE( \
167+
(env), \
168+
(stmt)->db_->IsInAuthorizerCallback() && \
169+
sqlite3_stmt_busy((stmt)->statement_.get()), \
170+
"database cannot be accessed from an authorizer callback")
171+
163172
// A statement's virtual machine cannot be reentered while sqlite3_step() is
164173
// running it. Finalizing it frees the VM outright, and re-running it resets the
165174
// VM mid-execution; both are use-after-free rather than merely a contract
@@ -2900,6 +2909,7 @@ void StatementSync::Close(const FunctionCallbackInfo<Value>& args) {
29002909
THROW_AND_RETURN_ON_BAD_STATE(
29012910
env, stmt->IsFinalized(), "statement has been finalized");
29022911
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2912+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29032913
stmt->Close();
29042914
}
29052915

@@ -2913,6 +2923,7 @@ void StatementSync::Dispose(const FunctionCallbackInfo<Value>& args) {
29132923
return;
29142924
}
29152925
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2926+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29162927
stmt->Close();
29172928
}
29182929

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,8 @@ class DatabaseSync : public BaseObject {
299299
voidDecrementAuthorizerDepth() { --authorizer_depth_; }
300300
boolIsInAuthorizerCallback() const { return authorizer_depth_ > 0; }
301301

302-
// Finalizing a statement frees its virtual machine, so a callback that
303-
// SQLite invokes from inside sqlite3_step() must not finalize the statement
304-
// being stepped. Other statements on the connection are safe to finalize.
302+
// A callback must not finalize the statement being stepped. Other statements
303+
// are safe unless they are busy during an authorizer callback.
305304
voidPushSteppingStatement(sqlite3_stmt* stmt) {
306305
stepping_statements_.push_back(stmt);
307306
}

β€Žtest/parallel/test-sqlite-authz.jsβ€Ž

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,8 @@ suite('authorizer callback reentrancy', () => {
397397
assert.deepStrictEqual(runInAuthorizer(db,cases),allRejected(cases));
398398
});
399399

400-
// Only the statement being stepped is unsafe to finalize. Other statements
401-
// on the connection have their own virtual machines, so finalizing them from
402-
// a callback is allowed.
403-
it('allows finalizing a statement that is not being executed',()=>{
400+
// An idle statement has no virtual-machine state or locks to release.
401+
it('allows finalizing an idle statement',()=>{
404402
constdb=newDatabaseSync(':memory:');
405403
db.exec('CREATE TABLE t (x INTEGER)');
406404
db.exec('INSERT INTO t VALUES (1)');
@@ -417,6 +415,43 @@ suite('authorizer callback reentrancy', () => {
417415
});
418416
});
419417

418+
// A paused iterator is busy and may hold locks between sqlite3_step() calls.
419+
it('rejects finalizing another active statement',()=>{
420+
for(constmethodof['close','dispose']){
421+
constdb=newDatabaseSync(':memory:');
422+
db.exec('CREATE TABLE t (x INTEGER)');
423+
db.exec('INSERT INTO t VALUES (1), (2), (3)');
424+
conststmt=db.prepare('SELECT x FROM t');
425+
constiter=stmt.iterate();
426+
iter.next();
427+
letoutcome='authorizer callback did not run';
428+
429+
db.setAuthorizer((actionCode)=>{
430+
if(actionCode===constants.SQLITE_DROP_TABLE){
431+
try{
432+
if(method==='close'){
433+
stmt.close();
434+
}else{
435+
stmt[Symbol.dispose]();
436+
}
437+
outcome='did not throw';
438+
}catch(err){
439+
outcome=`${err.code}: ${err.message}`;
440+
}
441+
}
442+
returnconstants.SQLITE_OK;
443+
});
444+
445+
assert.throws(()=>db.exec('DROP TABLE t'),{
446+
code: 'ERR_SQLITE_ERROR',
447+
message: 'database table is locked',
448+
});
449+
assert.strictEqual(outcome,expectedError);
450+
db.setAuthorizer(null);
451+
iter.return();
452+
}
453+
});
454+
420455
// Disposal is idempotent, so a statement that is already finalized must stay
421456
// a no-op even inside a callback. Throwing here would turn a `using` scope's
422457
// real exception into a SuppressedError.

0 commit comments

Comments
Β (0)
, '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

Commit fce6754

Browse files
trivikraduh95
authored andcommitted
sqlite: reject busy statement finalization in authorizer
An iterator can leave a statement active between sqlite3_step() calls. Finalizing that statement from an authorizer callback can release its locks and change the outcome of the statement being authorized. Reject close() and Symbol.dispose when the statement is busy and the connection is in an authorizer callback. Continue allowing idle statements to be finalized. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65369Fixes: #65368 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent 29f010f commit fce6754

4 files changed

Lines changed: 53 additions & 8 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,7 @@ Finalizes the prepared statement. An exception is thrown if the statement is
11201120
already finalized. An [`ERR_INVALID_STATE`][] error is thrown if this statement
11211121
is currently executing, which happens when the method is called from a callback
11221122
that the statement itself triggered, such as a user-defined function, an
1123-
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Other statements
1123+
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Idle statements
11241124
on the same connection can be finalized from such a callback. This method is a
11251125
wrapper around [`sqlite3_finalize()`][].
11261126

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,15 @@ inline MaybeLocal<Value> IntegerToValue(Isolate* isolate,
160160
(db)->IsInAuthorizerCallback(), \
161161
"database cannot be accessed from an authorizer callback")
162162

163+
// Finalizing a busy statement from an authorizer can release its locks and
164+
// change the outer statement's outcome.
165+
#defineTHROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt) \
166+
THROW_AND_RETURN_ON_BAD_STATE( \
167+
(env), \
168+
(stmt)->db_->IsInAuthorizerCallback() && \
169+
sqlite3_stmt_busy((stmt)->statement_.get()), \
170+
"database cannot be accessed from an authorizer callback")
171+
163172
// A statement's virtual machine cannot be reentered while sqlite3_step() is
164173
// running it. Finalizing it frees the VM outright, and re-running it resets the
165174
// VM mid-execution; both are use-after-free rather than merely a contract
@@ -2900,6 +2909,7 @@ void StatementSync::Close(const FunctionCallbackInfo<Value>& args) {
29002909
THROW_AND_RETURN_ON_BAD_STATE(
29012910
env, stmt->IsFinalized(), "statement has been finalized");
29022911
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2912+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29032913
stmt->Close();
29042914
}
29052915

@@ -2913,6 +2923,7 @@ void StatementSync::Dispose(const FunctionCallbackInfo<Value>& args) {
29132923
return;
29142924
}
29152925
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2926+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29162927
stmt->Close();
29172928
}
29182929

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,8 @@ class DatabaseSync : public BaseObject {
299299
voidDecrementAuthorizerDepth() { --authorizer_depth_; }
300300
boolIsInAuthorizerCallback() const { return authorizer_depth_ > 0; }
301301

302-
// Finalizing a statement frees its virtual machine, so a callback that
303-
// SQLite invokes from inside sqlite3_step() must not finalize the statement
304-
// being stepped. Other statements on the connection are safe to finalize.
302+
// A callback must not finalize the statement being stepped. Other statements
303+
// are safe unless they are busy during an authorizer callback.
305304
voidPushSteppingStatement(sqlite3_stmt* stmt) {
306305
stepping_statements_.push_back(stmt);
307306
}

β€Žtest/parallel/test-sqlite-authz.jsβ€Ž

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,8 @@ suite('authorizer callback reentrancy', () => {
397397
assert.deepStrictEqual(runInAuthorizer(db,cases),allRejected(cases));
398398
});
399399

400-
// Only the statement being stepped is unsafe to finalize. Other statements
401-
// on the connection have their own virtual machines, so finalizing them from
402-
// a callback is allowed.
403-
it('allows finalizing a statement that is not being executed',()=>{
400+
// An idle statement has no virtual-machine state or locks to release.
401+
it('allows finalizing an idle statement',()=>{
404402
constdb=newDatabaseSync(':memory:');
405403
db.exec('CREATE TABLE t (x INTEGER)');
406404
db.exec('INSERT INTO t VALUES (1)');
@@ -417,6 +415,43 @@ suite('authorizer callback reentrancy', () => {
417415
});
418416
});
419417

418+
// A paused iterator is busy and may hold locks between sqlite3_step() calls.
419+
it('rejects finalizing another active statement',()=>{
420+
for(constmethodof['close','dispose']){
421+
constdb=newDatabaseSync(':memory:');
422+
db.exec('CREATE TABLE t (x INTEGER)');
423+
db.exec('INSERT INTO t VALUES (1), (2), (3)');
424+
conststmt=db.prepare('SELECT x FROM t');
425+
constiter=stmt.iterate();
426+
iter.next();
427+
letoutcome='authorizer callback did not run';
428+
429+
db.setAuthorizer((actionCode)=>{
430+
if(actionCode===constants.SQLITE_DROP_TABLE){
431+
try{
432+
if(method==='close'){
433+
stmt.close();
434+
}else{
435+
stmt[Symbol.dispose]();
436+
}
437+
outcome='did not throw';
438+
}catch(err){
439+
outcome=`${err.code}: ${err.message}`;
440+
}
441+
}
442+
returnconstants.SQLITE_OK;
443+
});
444+
445+
assert.throws(()=>db.exec('DROP TABLE t'),{
446+
code: 'ERR_SQLITE_ERROR',
447+
message: 'database table is locked',
448+
});
449+
assert.strictEqual(outcome,expectedError);
450+
db.setAuthorizer(null);
451+
iter.return();
452+
}
453+
});
454+
420455
// Disposal is idempotent, so a statement that is already finalized must stay
421456
// a no-op even inside a callback. Throwing here would turn a `using` scope's
422457
// real exception into a SuppressedError.

0 commit comments

Comments
Β (0)
, '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

Commit fce6754

Browse files
trivikraduh95
authored andcommitted
sqlite: reject busy statement finalization in authorizer
An iterator can leave a statement active between sqlite3_step() calls. Finalizing that statement from an authorizer callback can release its locks and change the outcome of the statement being authorized. Reject close() and Symbol.dispose when the statement is busy and the connection is in an authorizer callback. Continue allowing idle statements to be finalized. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65369Fixes: #65368 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent 29f010f commit fce6754

4 files changed

Lines changed: 53 additions & 8 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,7 @@ Finalizes the prepared statement. An exception is thrown if the statement is
11201120
already finalized. An [`ERR_INVALID_STATE`][] error is thrown if this statement
11211121
is currently executing, which happens when the method is called from a callback
11221122
that the statement itself triggered, such as a user-defined function, an
1123-
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Other statements
1123+
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Idle statements
11241124
on the same connection can be finalized from such a callback. This method is a
11251125
wrapper around [`sqlite3_finalize()`][].
11261126

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,15 @@ inline MaybeLocal<Value> IntegerToValue(Isolate* isolate,
160160
(db)->IsInAuthorizerCallback(), \
161161
"database cannot be accessed from an authorizer callback")
162162

163+
// Finalizing a busy statement from an authorizer can release its locks and
164+
// change the outer statement's outcome.
165+
#defineTHROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt) \
166+
THROW_AND_RETURN_ON_BAD_STATE( \
167+
(env), \
168+
(stmt)->db_->IsInAuthorizerCallback() && \
169+
sqlite3_stmt_busy((stmt)->statement_.get()), \
170+
"database cannot be accessed from an authorizer callback")
171+
163172
// A statement's virtual machine cannot be reentered while sqlite3_step() is
164173
// running it. Finalizing it frees the VM outright, and re-running it resets the
165174
// VM mid-execution; both are use-after-free rather than merely a contract
@@ -2900,6 +2909,7 @@ void StatementSync::Close(const FunctionCallbackInfo<Value>& args) {
29002909
THROW_AND_RETURN_ON_BAD_STATE(
29012910
env, stmt->IsFinalized(), "statement has been finalized");
29022911
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2912+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29032913
stmt->Close();
29042914
}
29052915

@@ -2913,6 +2923,7 @@ void StatementSync::Dispose(const FunctionCallbackInfo<Value>& args) {
29132923
return;
29142924
}
29152925
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2926+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29162927
stmt->Close();
29172928
}
29182929

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,8 @@ class DatabaseSync : public BaseObject {
299299
voidDecrementAuthorizerDepth() { --authorizer_depth_; }
300300
boolIsInAuthorizerCallback() const { return authorizer_depth_ > 0; }
301301

302-
// Finalizing a statement frees its virtual machine, so a callback that
303-
// SQLite invokes from inside sqlite3_step() must not finalize the statement
304-
// being stepped. Other statements on the connection are safe to finalize.
302+
// A callback must not finalize the statement being stepped. Other statements
303+
// are safe unless they are busy during an authorizer callback.
305304
voidPushSteppingStatement(sqlite3_stmt* stmt) {
306305
stepping_statements_.push_back(stmt);
307306
}

β€Žtest/parallel/test-sqlite-authz.jsβ€Ž

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,8 @@ suite('authorizer callback reentrancy', () => {
397397
assert.deepStrictEqual(runInAuthorizer(db,cases),allRejected(cases));
398398
});
399399

400-
// Only the statement being stepped is unsafe to finalize. Other statements
401-
// on the connection have their own virtual machines, so finalizing them from
402-
// a callback is allowed.
403-
it('allows finalizing a statement that is not being executed',()=>{
400+
// An idle statement has no virtual-machine state or locks to release.
401+
it('allows finalizing an idle statement',()=>{
404402
constdb=newDatabaseSync(':memory:');
405403
db.exec('CREATE TABLE t (x INTEGER)');
406404
db.exec('INSERT INTO t VALUES (1)');
@@ -417,6 +415,43 @@ suite('authorizer callback reentrancy', () => {
417415
});
418416
});
419417

418+
// A paused iterator is busy and may hold locks between sqlite3_step() calls.
419+
it('rejects finalizing another active statement',()=>{
420+
for(constmethodof['close','dispose']){
421+
constdb=newDatabaseSync(':memory:');
422+
db.exec('CREATE TABLE t (x INTEGER)');
423+
db.exec('INSERT INTO t VALUES (1), (2), (3)');
424+
conststmt=db.prepare('SELECT x FROM t');
425+
constiter=stmt.iterate();
426+
iter.next();
427+
letoutcome='authorizer callback did not run';
428+
429+
db.setAuthorizer((actionCode)=>{
430+
if(actionCode===constants.SQLITE_DROP_TABLE){
431+
try{
432+
if(method==='close'){
433+
stmt.close();
434+
}else{
435+
stmt[Symbol.dispose]();
436+
}
437+
outcome='did not throw';
438+
}catch(err){
439+
outcome=`${err.code}: ${err.message}`;
440+
}
441+
}
442+
returnconstants.SQLITE_OK;
443+
});
444+
445+
assert.throws(()=>db.exec('DROP TABLE t'),{
446+
code: 'ERR_SQLITE_ERROR',
447+
message: 'database table is locked',
448+
});
449+
assert.strictEqual(outcome,expectedError);
450+
db.setAuthorizer(null);
451+
iter.return();
452+
}
453+
});
454+
420455
// Disposal is idempotent, so a statement that is already finalized must stay
421456
// a no-op even inside a callback. Throwing here would turn a `using` scope's
422457
// real exception into a SuppressedError.

0 commit comments

Comments
Β (0)
, '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

Commit fce6754

Browse files
trivikraduh95
authored andcommitted
sqlite: reject busy statement finalization in authorizer
An iterator can leave a statement active between sqlite3_step() calls. Finalizing that statement from an authorizer callback can release its locks and change the outcome of the statement being authorized. Reject close() and Symbol.dispose when the statement is busy and the connection is in an authorizer callback. Continue allowing idle statements to be finalized. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65369Fixes: #65368 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent 29f010f commit fce6754

4 files changed

Lines changed: 53 additions & 8 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,7 @@ Finalizes the prepared statement. An exception is thrown if the statement is
11201120
already finalized. An [`ERR_INVALID_STATE`][] error is thrown if this statement
11211121
is currently executing, which happens when the method is called from a callback
11221122
that the statement itself triggered, such as a user-defined function, an
1123-
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Other statements
1123+
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Idle statements
11241124
on the same connection can be finalized from such a callback. This method is a
11251125
wrapper around [`sqlite3_finalize()`][].
11261126

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,15 @@ inline MaybeLocal<Value> IntegerToValue(Isolate* isolate,
160160
(db)->IsInAuthorizerCallback(), \
161161
"database cannot be accessed from an authorizer callback")
162162

163+
// Finalizing a busy statement from an authorizer can release its locks and
164+
// change the outer statement's outcome.
165+
#defineTHROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt) \
166+
THROW_AND_RETURN_ON_BAD_STATE( \
167+
(env), \
168+
(stmt)->db_->IsInAuthorizerCallback() && \
169+
sqlite3_stmt_busy((stmt)->statement_.get()), \
170+
"database cannot be accessed from an authorizer callback")
171+
163172
// A statement's virtual machine cannot be reentered while sqlite3_step() is
164173
// running it. Finalizing it frees the VM outright, and re-running it resets the
165174
// VM mid-execution; both are use-after-free rather than merely a contract
@@ -2900,6 +2909,7 @@ void StatementSync::Close(const FunctionCallbackInfo<Value>& args) {
29002909
THROW_AND_RETURN_ON_BAD_STATE(
29012910
env, stmt->IsFinalized(), "statement has been finalized");
29022911
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2912+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29032913
stmt->Close();
29042914
}
29052915

@@ -2913,6 +2923,7 @@ void StatementSync::Dispose(const FunctionCallbackInfo<Value>& args) {
29132923
return;
29142924
}
29152925
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2926+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29162927
stmt->Close();
29172928
}
29182929

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,8 @@ class DatabaseSync : public BaseObject {
299299
voidDecrementAuthorizerDepth() { --authorizer_depth_; }
300300
boolIsInAuthorizerCallback() const { return authorizer_depth_ > 0; }
301301

302-
// Finalizing a statement frees its virtual machine, so a callback that
303-
// SQLite invokes from inside sqlite3_step() must not finalize the statement
304-
// being stepped. Other statements on the connection are safe to finalize.
302+
// A callback must not finalize the statement being stepped. Other statements
303+
// are safe unless they are busy during an authorizer callback.
305304
voidPushSteppingStatement(sqlite3_stmt* stmt) {
306305
stepping_statements_.push_back(stmt);
307306
}

β€Žtest/parallel/test-sqlite-authz.jsβ€Ž

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,8 @@ suite('authorizer callback reentrancy', () => {
397397
assert.deepStrictEqual(runInAuthorizer(db,cases),allRejected(cases));
398398
});
399399

400-
// Only the statement being stepped is unsafe to finalize. Other statements
401-
// on the connection have their own virtual machines, so finalizing them from
402-
// a callback is allowed.
403-
it('allows finalizing a statement that is not being executed',()=>{
400+
// An idle statement has no virtual-machine state or locks to release.
401+
it('allows finalizing an idle statement',()=>{
404402
constdb=newDatabaseSync(':memory:');
405403
db.exec('CREATE TABLE t (x INTEGER)');
406404
db.exec('INSERT INTO t VALUES (1)');
@@ -417,6 +415,43 @@ suite('authorizer callback reentrancy', () => {
417415
});
418416
});
419417

418+
// A paused iterator is busy and may hold locks between sqlite3_step() calls.
419+
it('rejects finalizing another active statement',()=>{
420+
for(constmethodof['close','dispose']){
421+
constdb=newDatabaseSync(':memory:');
422+
db.exec('CREATE TABLE t (x INTEGER)');
423+
db.exec('INSERT INTO t VALUES (1), (2), (3)');
424+
conststmt=db.prepare('SELECT x FROM t');
425+
constiter=stmt.iterate();
426+
iter.next();
427+
letoutcome='authorizer callback did not run';
428+
429+
db.setAuthorizer((actionCode)=>{
430+
if(actionCode===constants.SQLITE_DROP_TABLE){
431+
try{
432+
if(method==='close'){
433+
stmt.close();
434+
}else{
435+
stmt[Symbol.dispose]();
436+
}
437+
outcome='did not throw';
438+
}catch(err){
439+
outcome=`${err.code}: ${err.message}`;
440+
}
441+
}
442+
returnconstants.SQLITE_OK;
443+
});
444+
445+
assert.throws(()=>db.exec('DROP TABLE t'),{
446+
code: 'ERR_SQLITE_ERROR',
447+
message: 'database table is locked',
448+
});
449+
assert.strictEqual(outcome,expectedError);
450+
db.setAuthorizer(null);
451+
iter.return();
452+
}
453+
});
454+
420455
// Disposal is idempotent, so a statement that is already finalized must stay
421456
// a no-op even inside a callback. Throwing here would turn a `using` scope's
422457
// real exception into a SuppressedError.

0 commit comments

Comments
Β (0)
, '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

Commit fce6754

Browse files
trivikraduh95
authored andcommitted
sqlite: reject busy statement finalization in authorizer
An iterator can leave a statement active between sqlite3_step() calls. Finalizing that statement from an authorizer callback can release its locks and change the outcome of the statement being authorized. Reject close() and Symbol.dispose when the statement is busy and the connection is in an authorizer callback. Continue allowing idle statements to be finalized. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65369Fixes: #65368 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent 29f010f commit fce6754

4 files changed

Lines changed: 53 additions & 8 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,7 @@ Finalizes the prepared statement. An exception is thrown if the statement is
11201120
already finalized. An [`ERR_INVALID_STATE`][] error is thrown if this statement
11211121
is currently executing, which happens when the method is called from a callback
11221122
that the statement itself triggered, such as a user-defined function, an
1123-
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Other statements
1123+
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Idle statements
11241124
on the same connection can be finalized from such a callback. This method is a
11251125
wrapper around [`sqlite3_finalize()`][].
11261126

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,15 @@ inline MaybeLocal<Value> IntegerToValue(Isolate* isolate,
160160
(db)->IsInAuthorizerCallback(), \
161161
"database cannot be accessed from an authorizer callback")
162162

163+
// Finalizing a busy statement from an authorizer can release its locks and
164+
// change the outer statement's outcome.
165+
#defineTHROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt) \
166+
THROW_AND_RETURN_ON_BAD_STATE( \
167+
(env), \
168+
(stmt)->db_->IsInAuthorizerCallback() && \
169+
sqlite3_stmt_busy((stmt)->statement_.get()), \
170+
"database cannot be accessed from an authorizer callback")
171+
163172
// A statement's virtual machine cannot be reentered while sqlite3_step() is
164173
// running it. Finalizing it frees the VM outright, and re-running it resets the
165174
// VM mid-execution; both are use-after-free rather than merely a contract
@@ -2900,6 +2909,7 @@ void StatementSync::Close(const FunctionCallbackInfo<Value>& args) {
29002909
THROW_AND_RETURN_ON_BAD_STATE(
29012910
env, stmt->IsFinalized(), "statement has been finalized");
29022911
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2912+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29032913
stmt->Close();
29042914
}
29052915

@@ -2913,6 +2923,7 @@ void StatementSync::Dispose(const FunctionCallbackInfo<Value>& args) {
29132923
return;
29142924
}
29152925
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2926+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29162927
stmt->Close();
29172928
}
29182929

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,8 @@ class DatabaseSync : public BaseObject {
299299
voidDecrementAuthorizerDepth() { --authorizer_depth_; }
300300
boolIsInAuthorizerCallback() const { return authorizer_depth_ > 0; }
301301

302-
// Finalizing a statement frees its virtual machine, so a callback that
303-
// SQLite invokes from inside sqlite3_step() must not finalize the statement
304-
// being stepped. Other statements on the connection are safe to finalize.
302+
// A callback must not finalize the statement being stepped. Other statements
303+
// are safe unless they are busy during an authorizer callback.
305304
voidPushSteppingStatement(sqlite3_stmt* stmt) {
306305
stepping_statements_.push_back(stmt);
307306
}

β€Žtest/parallel/test-sqlite-authz.jsβ€Ž

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,8 @@ suite('authorizer callback reentrancy', () => {
397397
assert.deepStrictEqual(runInAuthorizer(db,cases),allRejected(cases));
398398
});
399399

400-
// Only the statement being stepped is unsafe to finalize. Other statements
401-
// on the connection have their own virtual machines, so finalizing them from
402-
// a callback is allowed.
403-
it('allows finalizing a statement that is not being executed',()=>{
400+
// An idle statement has no virtual-machine state or locks to release.
401+
it('allows finalizing an idle statement',()=>{
404402
constdb=newDatabaseSync(':memory:');
405403
db.exec('CREATE TABLE t (x INTEGER)');
406404
db.exec('INSERT INTO t VALUES (1)');
@@ -417,6 +415,43 @@ suite('authorizer callback reentrancy', () => {
417415
});
418416
});
419417

418+
// A paused iterator is busy and may hold locks between sqlite3_step() calls.
419+
it('rejects finalizing another active statement',()=>{
420+
for(constmethodof['close','dispose']){
421+
constdb=newDatabaseSync(':memory:');
422+
db.exec('CREATE TABLE t (x INTEGER)');
423+
db.exec('INSERT INTO t VALUES (1), (2), (3)');
424+
conststmt=db.prepare('SELECT x FROM t');
425+
constiter=stmt.iterate();
426+
iter.next();
427+
letoutcome='authorizer callback did not run';
428+
429+
db.setAuthorizer((actionCode)=>{
430+
if(actionCode===constants.SQLITE_DROP_TABLE){
431+
try{
432+
if(method==='close'){
433+
stmt.close();
434+
}else{
435+
stmt[Symbol.dispose]();
436+
}
437+
outcome='did not throw';
438+
}catch(err){
439+
outcome=`${err.code}: ${err.message}`;
440+
}
441+
}
442+
returnconstants.SQLITE_OK;
443+
});
444+
445+
assert.throws(()=>db.exec('DROP TABLE t'),{
446+
code: 'ERR_SQLITE_ERROR',
447+
message: 'database table is locked',
448+
});
449+
assert.strictEqual(outcome,expectedError);
450+
db.setAuthorizer(null);
451+
iter.return();
452+
}
453+
});
454+
420455
// Disposal is idempotent, so a statement that is already finalized must stay
421456
// a no-op even inside a callback. Throwing here would turn a `using` scope's
422457
// real exception into a SuppressedError.

0 commit comments

Comments
Β (0)
, '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

Commit fce6754

Browse files
trivikraduh95
authored andcommitted
sqlite: reject busy statement finalization in authorizer
An iterator can leave a statement active between sqlite3_step() calls. Finalizing that statement from an authorizer callback can release its locks and change the outcome of the statement being authorized. Reject close() and Symbol.dispose when the statement is busy and the connection is in an authorizer callback. Continue allowing idle statements to be finalized. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65369Fixes: #65368 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent 29f010f commit fce6754

4 files changed

Lines changed: 53 additions & 8 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,7 @@ Finalizes the prepared statement. An exception is thrown if the statement is
11201120
already finalized. An [`ERR_INVALID_STATE`][] error is thrown if this statement
11211121
is currently executing, which happens when the method is called from a callback
11221122
that the statement itself triggered, such as a user-defined function, an
1123-
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Other statements
1123+
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Idle statements
11241124
on the same connection can be finalized from such a callback. This method is a
11251125
wrapper around [`sqlite3_finalize()`][].
11261126

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,15 @@ inline MaybeLocal<Value> IntegerToValue(Isolate* isolate,
160160
(db)->IsInAuthorizerCallback(), \
161161
"database cannot be accessed from an authorizer callback")
162162

163+
// Finalizing a busy statement from an authorizer can release its locks and
164+
// change the outer statement's outcome.
165+
#defineTHROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt) \
166+
THROW_AND_RETURN_ON_BAD_STATE( \
167+
(env), \
168+
(stmt)->db_->IsInAuthorizerCallback() && \
169+
sqlite3_stmt_busy((stmt)->statement_.get()), \
170+
"database cannot be accessed from an authorizer callback")
171+
163172
// A statement's virtual machine cannot be reentered while sqlite3_step() is
164173
// running it. Finalizing it frees the VM outright, and re-running it resets the
165174
// VM mid-execution; both are use-after-free rather than merely a contract
@@ -2900,6 +2909,7 @@ void StatementSync::Close(const FunctionCallbackInfo<Value>& args) {
29002909
THROW_AND_RETURN_ON_BAD_STATE(
29012910
env, stmt->IsFinalized(), "statement has been finalized");
29022911
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2912+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29032913
stmt->Close();
29042914
}
29052915

@@ -2913,6 +2923,7 @@ void StatementSync::Dispose(const FunctionCallbackInfo<Value>& args) {
29132923
return;
29142924
}
29152925
THROW_AND_RETURN_IF_STEPPING(env, stmt);
2926+
THROW_AND_RETURN_IF_BUSY_IN_AUTHORIZER(env, stmt);
29162927
stmt->Close();
29172928
}
29182929

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,8 @@ class DatabaseSync : public BaseObject {
299299
voidDecrementAuthorizerDepth() { --authorizer_depth_; }
300300
boolIsInAuthorizerCallback() const { return authorizer_depth_ > 0; }
301301

302-
// Finalizing a statement frees its virtual machine, so a callback that
303-
// SQLite invokes from inside sqlite3_step() must not finalize the statement
304-
// being stepped. Other statements on the connection are safe to finalize.
302+
// A callback must not finalize the statement being stepped. Other statements
303+
// are safe unless they are busy during an authorizer callback.
305304
voidPushSteppingStatement(sqlite3_stmt* stmt) {
306305
stepping_statements_.push_back(stmt);
307306
}

β€Žtest/parallel/test-sqlite-authz.jsβ€Ž

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,8 @@ suite('authorizer callback reentrancy', () => {
397397
assert.deepStrictEqual(runInAuthorizer(db,cases),allRejected(cases));
398398
});
399399

400-
// Only the statement being stepped is unsafe to finalize. Other statements
401-
// on the connection have their own virtual machines, so finalizing them from
402-
// a callback is allowed.
403-
it('allows finalizing a statement that is not being executed',()=>{
400+
// An idle statement has no virtual-machine state or locks to release.
401+
it('allows finalizing an idle statement',()=>{
404402
constdb=newDatabaseSync(':memory:');
405403
db.exec('CREATE TABLE t (x INTEGER)');
406404
db.exec('INSERT INTO t VALUES (1)');
@@ -417,6 +415,43 @@ suite('authorizer callback reentrancy', () => {
417415
});
418416
});
419417

418+
// A paused iterator is busy and may hold locks between sqlite3_step() calls.
419+
it('rejects finalizing another active statement',()=>{
420+
for(constmethodof['close','dispose']){
421+
constdb=newDatabaseSync(':memory:');
422+
db.exec('CREATE TABLE t (x INTEGER)');
423+
db.exec('INSERT INTO t VALUES (1), (2), (3)');
424+
conststmt=db.prepare('SELECT x FROM t');
425+
constiter=stmt.iterate();
426+
iter.next();
427+
letoutcome='authorizer callback did not run';
428+
429+
db.setAuthorizer((actionCode)=>{
430+
if(actionCode===constants.SQLITE_DROP_TABLE){
431+
try{
432+
if(method==='close'){
433+
stmt.close();
434+
}else{
435+
stmt[Symbol.dispose]();
436+
}
437+
outcome='did not throw';
438+
}catch(err){
439+
outcome=`${err.code}: ${err.message}`;
440+
}
441+
}
442+
returnconstants.SQLITE_OK;
443+
});
444+
445+
assert.throws(()=>db.exec('DROP TABLE t'),{
446+
code: 'ERR_SQLITE_ERROR',
447+
message: 'database table is locked',
448+
});
449+
assert.strictEqual(outcome,expectedError);
450+
db.setAuthorizer(null);
451+
iter.return();
452+
}
453+
});
454+
420455
// Disposal is idempotent, so a statement that is already finalized must stay
421456
// a no-op even inside a callback. Throwing here would turn a `using` scope's
422457
// real exception into a SuppressedError.

0 commit comments

Comments
Β (0)