-
-
Notifications
You must be signed in to change notification settings - Fork 36.4k
sqlite: fix crash on db.close() from inside a user function #63183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -221,6 +221,24 @@ class DatabaseSync : public BaseObject { | |
| } | ||
| sqlite3* Connection(); | ||
|
|
||
| // SQLite forbids closing the database while a user-defined scalar or | ||
| // aggregate function callback is on the stack. Wrap every such | ||
| // callback with the RAII guard returned by EnterUserFunctionCallback(). | ||
| // db.close()/deserialize() and SQL tag store .clear() check | ||
| // IsInUserFunctionCallback() and refuse to run, since they would | ||
| // finalize statements (potentially the running one). Reentry into the | ||
| // *running* statement (recursive step, reset, or finalize) is | ||
| // detected separately via the per-statement | ||
| // StatementSync::IsStepping() flag, which leaves cross-statement use | ||
| // (the "lookup" pattern) unaffected. | ||
| inline auto EnterUserFunctionCallback() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This branch is based on
Concretely, the renamed message here fails an existing test: Worth rebasing and building on the existing guard rather than in parallel with it. Two things not to lose in the process: |
||
| user_function_callback_depth_++; | ||
| return OnScopeLeave([this]() { user_function_callback_depth_--; }); | ||
| } | ||
| bool IsInUserFunctionCallback() const { | ||
| return user_function_callback_depth_ > 0; | ||
| } | ||
|
|
||
| // In some situations, such as when using custom functions, it is possible | ||
| // that SQLite reports an error while JavaScript already has a pending | ||
| // exception. In this case, the SQLite error should be ignored. These methods | ||
|
|
@@ -241,6 +259,7 @@ class DatabaseSync : public BaseObject { | |
| bool enable_load_extension_; | ||
| sqlite3* connection_; | ||
| bool ignore_next_sqlite_error_; | ||
| int user_function_callback_depth_ = 0; | ||
|
|
||
| std::set<BackupJob*> backups_; | ||
| std::set<sqlite3_session*> sessions_; | ||
|
|
@@ -283,6 +302,18 @@ class StatementSync : public BaseObject { | |
| bool GetCachedColumnNames(v8::LocalVector<v8::Name>* keys); | ||
| void Finalize(); | ||
| bool IsFinalized(); | ||
| bool IsStepping() const { return stepping_; } | ||
|
|
||
| // RAII guard: marks this statement as being stepped while alive. | ||
| // JS-callable methods that would step, reset, or finalize this | ||
| // statement check IsStepping() and throw — that's the | ||
| // sqlite3_step / sqlite3_reset / sqlite3_finalize reentry SQLite | ||
| // forbids while the statement's user-defined function callback is | ||
| // on the stack. | ||
| inline auto MarkStepping() { | ||
| stepping_ = true; | ||
| return OnScopeLeave([this]() { stepping_ = false; }); | ||
| } | ||
|
|
||
| SET_MEMORY_INFO_NAME(StatementSync) | ||
| SET_SELF_SIZE(StatementSync) | ||
|
|
@@ -295,6 +326,7 @@ class StatementSync : public BaseObject { | |
| bool use_big_ints_; | ||
| bool allow_bare_named_params_; | ||
| bool allow_unknown_named_params_; | ||
| bool stepping_ = false; | ||
| uint64_t reset_generation_ = 0; | ||
| std::optional<std::map<std::string, std::string>> bare_named_params_; | ||
| inline int ResetStatement(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This guard doesn't cover the authorizer callback, and that leaves the original use-after-free reachable.
The authorizer runs during statement preparation, and preparation can happen inside
sqlite3_step: onSQLITE_SCHEMAthe publicsqlite3_stepwrapper callssqlite3Repreparein a loop (deps/sqlite/sqlite3.c:94612-94614), which reachessqlite3AuthCheck. So:Row 1's
f()does DDL, which expires all prepared statements. On row 2sqlite3_stepreturnsSQLITE_SCHEMA, reprepares in place, and invokes the authorizer — whereIsInUserFunctionCallback()is false, sodb.close()goes through.FinalizeStatements()then finalizes the very VM whosesqlite3_stepframe is live (crash 1 from the description), and zeroesconnection_. ThroughStatementSync::Runthe follow-onsqlite3_last_insert_rowid(db->Connection())derefs null (crash 2) — commit 2 dropped the connection-null check commit 1 added.The commit message defers the authorizer to a separate change, but this isn't a coverage nicety: it's the same crash, still reachable from pure JS, with the interim mitigations removed. Note
mainalready wrapsAuthorizerCallback(src/node_sqlite.cc:2552) — see my other comment.