Repository navigation
libsql: null sqlite3 handle after close to prevent double-free - #62
Merged
mostafa-yassine merged 4 commits intoOct 21, 2026
Conversation
Set raw pointer to null after sqlite3_close_v2 in disconnect() to prevent use-after-free when the handle is closed twice. This fixes issue tursodatabase#2251 where LibsqlConnection::Drop calls disconnect() and then the inner Connection::Drop also calls disconnect(), causing a double-close of the sqlite3 handle. The fix ensures disconnect() is idempotent by nulling the raw pointer after the first close, so subsequent calls are safe no-ops. (cherry picked from commit 4e06af5)
sle-c
approved these changes
Oct 20, 2026
This was referenced Oct 21, 2026
mostafa-yassine
merged commit Oct 21, 2026
fe0405e
into
v0.9.30-shopify-patches
30 of 33 checks passed
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.
Context
This fork is embedded in a mobile app that uses libsql remote replicas. The app drops its
libsql::Connectionandlibsql::Databaseand builds new ones every time it refreshes its auth token.Problem
Dropping a local connection closes the
sqlite3handle twice.LibsqlConnection::drop(libsql/src/local/impls.rs:108) callsself.conn.disconnect(). This callssqlite3_close_v2(self.raw)and frees the handle.conn: local::Connectionis then dropped.Connection::drop(libsql/src/local/connection.rs:38) callsdisconnect()again.self.rawstill holds the freed pointer anddrop_refis still unique, sosqlite3_close_v2runs on freed memory.Upstream tracks this as tursodatabase#2251. Valgrind shows one
Invalid read of size 1insqlite3SafetyCheckSickOrOkper connection.In production this shows up as a native SIGSEGV on Android. The symbolicated stack is:
The crash is intermittent because the allocator usually keeps the freed page mapped. It faults only when the page was released or reused, so it is rare per device and common across a fleet.
Solution
Cherry-pick upstream tursodatabase#2261 (
4e06af572d):disconnect()setsself.rawto null aftersqlite3_close_v2. The second call passes a null handle, whichsqlite3_close_v2accepts as a no-op.This PR adds a unit test,
local::connection::tests::disconnect_is_idempotent, that fails on the base branch and passes with the fix, and a second test that drops through the public API so a sanitizer run exercises bothDropimpls.This PR does not change the
LibsqlConnectionandConnectionDropimpls. Both still calldisconnect(). The second call is now a no-op.Testing
Two unit tests in
local::connection::tests:disconnect_is_idempotentcallsdisconnect()twice and checksrawis null after each call. Fails on the base branch (assertion failed: conn.raw.is_null()), passes with the fix.dropping_public_connection_closes_oncebuilds a local database throughBuilder::new_local, connects, runs one statement, and drops, 20 times, so bothDropimpls run. It passes on any allocator. Under a guard allocator every freed page is unmapped, so the second close faults if the handle is not nulled:Base branch: SIGSEGV, exit 139. lldb:
This is the production stack frame for frame. With this PR:
1 passed, exit 0.Checks.
cargo check -p libsql --features replicationandcargo fmt -p libsql -- --checkpass.Follow-up. Cut a
libsql-rs-v0.9.30+shopify.Ntag so consumers can bump.Related issues