fix: don't touch a closed connection when unsubscribing a reactive query - #442
Merged
ospfranco merged 3 commits intoAug 20, 2026
Merged
Conversation
Contributor
|
Awesome, thanks for the fix |
Contributor
|
One finding The same class of bug the PR fixes for updateHook (via the new auto_register_update_hook guard) still exists, unfixed, for commitHook/rollbackHook. Could you also implement it there so that the fix is complete? |
Contributor
Author
|
Done — you were right: db.close(); db.commitHook(cb) hits sqlite3_commit_hook(nullptr, …), same null read at 0x18. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
reactiveExecutereturns anunsubscribehost function that capturesDBHostObject*raw (HFN2(this, reactiveQuery)). Host functions don't retainthe host object, so JS can hold that
unsubscribeafter theDBit came fromis gone — a React effect cleanup outliving the database is the everyday case.
~DBHostObject()closes the connection and nulls thesqlite3*; the nextunsubscribe()reachesauto_register_update_hook(), which callssqlite3_update_hook(db, …)with no check ondb. That starts withsqlite3_mutex_enter(db->mutex), andmutexis at offset0x18instruct sqlite3, so the process dies reading0x18:EXC_BAD_ACCESS / KERN_INVALID_ADDRESS at 0x18on iOS, SIGSEGV on Android. We see this inproduction on iOS 18.
The same call path is reachable without waiting for a GC:
close()nulls thehandle but leaves the query registered, so a later
unsubscribe()callsopsqlite_deregister_update_hook(nullptr). The added test does exactly that andcrashes without this change.
What this changes
auto_register_update_hook()returns early when the connection is gone(
invalidated || db == nullptr).unsubscribecapturesweak_from_this()instead ofthis, so unsubscribingafter the database is destroyed is a no-op rather than a use-after-free — the
half a null check can't cover, since freed-and-reused memory isn't null. Every
DBHostObjectis created withstd::make_shared, so live databases areunaffected.
close/delete/invalidatedrop the hook state, releasing the JS callbackjsi::Values at close time instead of at finalization.delete()gives up the handle beforeopsqlite_remove(), which closes theconnection; it previously left a dangling pointer, including when
opsqlite_removethrows on a missing file.