From 760d4e45f771252b6efdff9e374535abb13a747c Mon Sep 17 00:00:00 2001 From: Wasim Date: Sun, 26 Jul 2026 10:09:40 +0530 Subject: [PATCH 1/4] fix: null sqlite3 handle after close to prevent double-free 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 #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 4e06af572ddf7e3b82a8609c4c623637bae0d778) --- libsql/src/local/connection.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libsql/src/local/connection.rs b/libsql/src/local/connection.rs index d9b8af80bd..ca1601a6f3 100644 --- a/libsql/src/local/connection.rs +++ b/libsql/src/local/connection.rs @@ -108,7 +108,10 @@ impl Connection { /// Disconnect from the database. pub fn disconnect(&mut self) { if Arc::get_mut(&mut self.drop_ref).is_some() { - unsafe { libsql_sys::ffi::sqlite3_close_v2(self.raw) }; + unsafe { + libsql_sys::ffi::sqlite3_close_v2(self.raw); + self.raw = std::ptr::null_mut(); + }; } } From eaabc928478aba68633a0b07890a0d4fa4bdd201 Mon Sep 17 00:00:00 2001 From: Mostafa Yassine Date: Tue, 20 Oct 2026 15:54:29 -0400 Subject: [PATCH 2/4] libsql: add regression test for idempotent Connection::disconnect --- libsql/src/local/connection.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/libsql/src/local/connection.rs b/libsql/src/local/connection.rs index ca1601a6f3..3583192d79 100644 --- a/libsql/src/local/connection.rs +++ b/libsql/src/local/connection.rs @@ -822,6 +822,24 @@ mod tests { OpenFlags, }; + /// `LibsqlConnection::drop` calls `disconnect()`, then the inner + /// `Connection::drop` calls it again. The second call must not reach + /// `sqlite3_close_v2` with a handle that the first call already freed. + #[test] + fn disconnect_is_idempotent() { + let temp_dir = tempfile::tempdir().unwrap(); + let path = temp_dir.path().join("local.db"); + let db = Database::new(path.to_str().unwrap().to_string(), OpenFlags::default()); + let mut conn = Connection::connect(&db).unwrap(); + assert!(!conn.raw.is_null()); + + conn.disconnect(); + assert!(conn.raw.is_null()); + + conn.disconnect(); + assert!(conn.raw.is_null()); + } + #[tokio::test] pub async fn test_kek() { let temp_dir = tempfile::tempdir().unwrap(); From b69bdb5bf6b916fa32ba120eac6e202e2f65a80d Mon Sep 17 00:00:00 2001 From: Mostafa Yassine Date: Tue, 20 Oct 2026 16:14:19 -0400 Subject: [PATCH 3/4] libsql: add double-close repro example for #2251 --- libsql/examples/double_close_repro.rs | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 libsql/examples/double_close_repro.rs diff --git a/libsql/examples/double_close_repro.rs b/libsql/examples/double_close_repro.rs new file mode 100644 index 0000000000..c70c41ab85 --- /dev/null +++ b/libsql/examples/double_close_repro.rs @@ -0,0 +1,33 @@ +// Repro for tursodatabase/libsql#2251: dropping a local connection closed the +// sqlite3 handle twice (LibsqlConnection::drop, then the inner +// Connection::drop). The second sqlite3_close_v2 read freed memory. +// +// Run under a guard allocator so the use-after-free faults on the first +// iteration instead of depending on allocator reuse: +// +// cargo build -p libsql --example double_close_repro --features replication +// DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib \ +// ./target/debug/examples/double_close_repro # macOS +// valgrind ./target/debug/examples/double_close_repro # Linux +// +// Without the fix: SIGSEGV in sqlite3SafetyCheckSickOrOk (sqlite3.c:36835). +// With the fix: prints "no crash". +#[tokio::main] +async fn main() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("repro.db"); + for i in 0..20 { + let db = libsql::Builder::new_local(path.to_str().unwrap()) + .build() + .await + .unwrap(); + let conn = db.connect().unwrap(); + conn.execute("CREATE TABLE IF NOT EXISTS t(x INTEGER)", ()) + .await + .unwrap(); + drop(conn); + drop(db); + println!("iteration {i} ok"); + } + println!("no crash"); +} From 5863c874215ceb6e7fc1f0903945e179afbe9578 Mon Sep 17 00:00:00 2001 From: Mostafa Yassine Date: Tue, 20 Oct 2026 21:09:38 -0400 Subject: [PATCH 4/4] libsql: fold double-close repro into a unit test --- libsql/examples/double_close_repro.rs | 33 --------------------------- libsql/src/local/connection.rs | 21 +++++++++++++++++ 2 files changed, 21 insertions(+), 33 deletions(-) delete mode 100644 libsql/examples/double_close_repro.rs diff --git a/libsql/examples/double_close_repro.rs b/libsql/examples/double_close_repro.rs deleted file mode 100644 index c70c41ab85..0000000000 --- a/libsql/examples/double_close_repro.rs +++ /dev/null @@ -1,33 +0,0 @@ -// Repro for tursodatabase/libsql#2251: dropping a local connection closed the -// sqlite3 handle twice (LibsqlConnection::drop, then the inner -// Connection::drop). The second sqlite3_close_v2 read freed memory. -// -// Run under a guard allocator so the use-after-free faults on the first -// iteration instead of depending on allocator reuse: -// -// cargo build -p libsql --example double_close_repro --features replication -// DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib \ -// ./target/debug/examples/double_close_repro # macOS -// valgrind ./target/debug/examples/double_close_repro # Linux -// -// Without the fix: SIGSEGV in sqlite3SafetyCheckSickOrOk (sqlite3.c:36835). -// With the fix: prints "no crash". -#[tokio::main] -async fn main() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("repro.db"); - for i in 0..20 { - let db = libsql::Builder::new_local(path.to_str().unwrap()) - .build() - .await - .unwrap(); - let conn = db.connect().unwrap(); - conn.execute("CREATE TABLE IF NOT EXISTS t(x INTEGER)", ()) - .await - .unwrap(); - drop(conn); - drop(db); - println!("iteration {i} ok"); - } - println!("no crash"); -} diff --git a/libsql/src/local/connection.rs b/libsql/src/local/connection.rs index 3583192d79..3d06b5ac52 100644 --- a/libsql/src/local/connection.rs +++ b/libsql/src/local/connection.rs @@ -840,6 +840,27 @@ mod tests { assert!(conn.raw.is_null()); } + /// Drops a connection through the public API so both `Drop` impls run. + /// Passes on any allocator; under Guard Malloc or valgrind it faults in + /// `sqlite3SafetyCheckSickOrOk` if `disconnect()` closes the handle twice. + #[tokio::test] + async fn dropping_public_connection_closes_once() { + let temp_dir = tempfile::tempdir().unwrap(); + let path = temp_dir.path().join("local.db"); + for _ in 0..20 { + let db = crate::Builder::new_local(path.to_str().unwrap()) + .build() + .await + .unwrap(); + let conn = db.connect().unwrap(); + conn.execute("CREATE TABLE IF NOT EXISTS t(x INTEGER)", ()) + .await + .unwrap(); + drop(conn); + drop(db); + } + } + #[tokio::test] pub async fn test_kek() { let temp_dir = tempfile::tempdir().unwrap();