Skip to content

fix: null sqlite3 handle after close to prevent double-free (#2251)#2261

Open
wasim-builds wants to merge 1 commit into
tursodatabase:mainfrom
wasim-builds:fix-double-close
Open

fix: null sqlite3 handle after close to prevent double-free (#2251)#2261
wasim-builds wants to merge 1 commit into
tursodatabase:mainfrom
wasim-builds:fix-double-close

Conversation

@wasim-builds

Copy link
Copy Markdown

Problem

Closes #2251

The local connection's sqlite3 handle is sqlite3_close_v2'd twice on teardown. LibsqlConnection's Drop calls disconnect(), then the inner Connection's Drop calls it again. Both go through Connection::disconnect (connection.rs:110). disconnect() isn't idempotent — the Arc::get_mut(drop_ref) guard only checks unique ownership (true both times) and doesn't record that the handle was already closed.

On Windows this is a hard STATUS_ACCESS_VIOLATION (0xC0000005); on Linux it doesn't fault (glibc keeps the freed page mapped) but the double free is real, as valgrind shows — 800 errors from 1 context at 800 iterations, one per connection.

Root Cause

LibsqlConnection::drop -> self.conn.disconnect()   <- first close
Connection::drop       -> self.disconnect()         <- second close

Both calls succeed because Arc::get_mut(drop_ref) returns Some both times (the first close doesn't mark the handle as closed).

Fix

After calling sqlite3_close_v2, set self.raw to std::ptr::null_mut(). This makes disconnect() idempotent — the second call sees a null pointer, and even if Arc::get_mut passes, calling sqlite3_close_v2(null) is documented as safe (no-op) in SQLite.

Changes

  • libsql/src/local/connection.rs: Null raw after sqlite3_close_v2 in disconnect()

Reproduction (from issue)

for _ in 0..50_000 {
    let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
    rt.block_on(async {
        let db = libsql::Builder::new_local(":memory:").build().await.unwrap();
        let conn = db.connect().unwrap();
        conn.execute("CREATE TABLE t(x INTEGER)", ()).await.unwrap();
    });
}

With valgrind: 800 errors at 800 iterations. On Windows: STATUS_ACCESS_VIOLATION within first few hundred iterations.

Testing

  • cargo check passes
  • The fix is minimal and makes disconnect() idempotent as suggested in the issue

Co-Authored-By: Claude noreply@anthropic.com

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Local connection double-close (use-after-free) on teardown — sqlite3_close_v2 called twice

1 participant