You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
Repro
Cargo.toml:src/main.rs:What happens
The local connection's
sqlite3handle issqlite3_close_v2'd twice on teardown. Under valgrind this is fully deterministic — 800 errors from 1 context at 800 iterations, one per connection:LibsqlConnection'sDropcallsdisconnect(), then the innerConnection'sDropcalls it again, both viaConnection::disconnect(connection.rs:110).disconnect()isn't idempotent, and theArc::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 --releaseexits-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
x86_64-pc-windows-msvcx86_64(valgrind /target/debug/lsrepro 800)["core", "remote", "tls"]Ruled out
local::Database::newguardssqlite3_config(SQLITE_CONFIG_SERIALIZED)+sqlite3_initialize()behind aOnce.-DSQLITE_THREADSAFE=1.RefCell→RwLock) is already in 0.9.30.Looks like either
disconnect()should be idempotent (nullrawaftersqlite3_close_v2) or only one ofLibsqlConnection/Connectionshould own the close. Happy to test a patch.