tursodatabase / tursodatabase/libsql

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

Open
#2,251 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C
Stars
17.2k
Forks
531
Avg merge
1h 12m
Merged PRs (30d)
1

Description

Repro

Cargo.toml:

[dependencies]
libsql = { version = "0.9.30", default-features = false, features = ["core"] }
tokio  = { version = "1", features = ["rt", "macros"] }

src/main.rs:

fn main() {
    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();
        });
    }
}

What happens

The local connection's sqlite3 handle is sqlite3_close_v2'd twice on teardown. Under valgrind this is fully deterministic — 800 errors from 1 context at 800 iterations, one per connection:

Invalid read of size 1
   at  sqlite3SafetyCheckSickOrOk (sqlite3.c)
   by  sqlite3_close_v2
   by  libsql::local::connection::Connection::disconnect (connection.rs:110)
   by  <Connection as Drop>::drop (connection.rs:40)            <- second close
 Address ... is freed by
   by  sqlite3_close_v2
   by  libsql::local::connection::Connection::disconnect (connection.rs:110)
   by  <LibsqlConnection as Drop>::drop (impls.rs:110)          <- first close
 Block was alloc'd at
   by  sqlite3_open_v2
   by  libsql::local::connection::Connection::connect (connection.rs:55)

LibsqlConnection's Drop calls disconnect(), then the inner Connection's Drop calls it again, both via Connection::disconnect (connection.rs:110). disconnect() isn't idempotent, and the Arc::get_mut(drop_ref) guard only checks unique ownership (true both times) — it doesn't record that the handle was already closed.

On Windows this is a hard STATUS_ACCESS_VIOLATION (0xC0000005): cargo run --release exits -1073741819, usually within the first few hundred iterations (it's stochastic — rerun if a run finishes). On Linux it doesn't fault (glibc keeps the freed page mapped) so it passes silently, but the double free is real, as valgrind shows. Building and dropping a fresh current-thread runtime per iteration (what every #[tokio::test] does) is what made it reliable for me.

Environment

  • libsql / libsql-ffi / libsql-sys 0.9.30, stable Rust
  • Native crash: Windows 11, x86_64-pc-windows-msvc
  • valgrind: Linux x86_64 (valgrind /target/debug/lsrepro 800)
  • Same behavior with features ["core", "remote", "tls"]

Ruled out

  • Not the cold-init race — local::Database::new guards sqlite3_config(SQLITE_CONFIG_SERIALIZED) + sqlite3_initialize() behind a Once.
  • ffi is built with -DSQLITE_THREADSAFE=1.
  • #2118 (RefCellRwLock) is already in 0.9.30.
  • Not test-harness parallelism — the repro is single-threaded.

Looks like either disconnect() should be idempotent (null raw after sqlite3_close_v2) or only one of LibsqlConnection / Connection should own the close. Happy to test a patch.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with local::connection::Connection in connection.rs, especially disconnect at line 110 and its Drop implementation, then inspect LibsqlConnection's Drop in impls.rs. Run the provided 50,000-iteration reproduction under Valgrind or on Windows and trace both teardown paths. Done means the handle is closed only once without the reported invalid read or access violation.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust, sqlite
Domain
databases
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.