sqlite: fix crash on db.close() from inside a user function#63183
sqlite: fix crash on db.close() from inside a user function#63183mceachen wants to merge 1 commit intonodejs:mainfrom
Conversation
Calling db.close() from inside a user-defined function callback while sqlite3_step is on the call stack caused two distinct crashes: 1. DatabaseSync::Close ran sqlite3_finalize on the statement whose sqlite3_step frame was still active, freeing the VM that step was executing. The outer step then operated on freed memory. 2. Even if (1) is avoided, StatementExecutionHelper::Run dereferenced db->Connection() via sqlite3_last_insert_rowid / sqlite3_changes64 after step returned. The reentrant close zeroed connection_, so the deref crashed. Add a MarkStepping() RAII guard wrapped around every sqlite3_step caller. If Finalize() is called while stepping_, defer it; the guard's destructor runs the deferred finalize after step returns. Add a connection-null check in StatementExecutionHelper::Run before the connection-dependent reads, throwing ERR_INVALID_STATE. Fixes: nodejs#63180 Signed-off-by: Matthew McEachen <matthew@photostructure.com>
|
Review requested:
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #63183 +/- ##
==========================================
- Coverage 90.03% 90.03% -0.01%
==========================================
Files 713 713
Lines 224497 224723 +226
Branches 42426 42479 +53
==========================================
+ Hits 202122 202322 +200
- Misses 14181 14187 +6
- Partials 8194 8214 +20
🚀 New features to boost your workflow:
|
|
This definitely shouldn't segfault, but it's worth saying that "don't close the db handle during a user function callback" is expressly part of the sqlite3_create_function API contract, as is finalizing/resetting the statement, which I think we are also able to do: let stmt;
db.function('x', () => stmt.get());
stmt = db.prepare('SELECT x()');
stmt.get();Rather than performing forbidden operations and then mitigating against the consequences, it would probably be better to prevent those operations from occurring in the first place. Could we instead set some sort of state while a user callback function is being called, that causes attempts to close/finalize to be rejected with an exception? |
Calling db.close() from inside a user-defined function callback while sqlite3_step is on the call stack caused two distinct crashes:
DatabaseSync::Close ran sqlite3_finalize on the statement whose sqlite3_step frame was still active, freeing the VM that step was executing. The outer step then operated on freed memory.
Even if (1) is avoided, StatementExecutionHelper::Run dereferenced db->Connection() via sqlite3_last_insert_rowid / sqlite3_changes64 after step returned. The reentrant close zeroed connection_, so the deref crashed.
Add a MarkStepping() RAII guard wrapped around every sqlite3_step caller. If Finalize() is called while stepping_, defer it; the guard's destructor runs the deferred finalize after step returns. Add a connection-null check in StatementExecutionHelper::Run before the connection-dependent reads, throwing ERR_INVALID_STATE.
Fixes: #63180
(full disclosure: produced with assistance from claude and codex)