Skip to content

libsql: close a local connection's sqlite3 handle exactly once - #2282

Open
penberg wants to merge 1 commit into
mainfrom
fix-double-close-on-drop
Open

penberg wants to merge 1 commit into
mainfrom
fix-double-close-on-drop

Conversation

@penberg

@penberg penberg commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

LibsqlConnection::drop called local::Connection::disconnect(), and then the inner local::Connection's own Drop called disconnect() again on the same raw handle, so every connection close ran sqlite3_close_v2() twice. The second call reads the freed sqlite3 struct. It usually returns SQLITE_MISUSE harmlessly, but when another thread has reused the freed block first, sqlite walks garbage and crashes. This surfaced in libsql-js as an intermittent access violation at worker-thread exit on Windows, where the finalizers close many connections in a burst.

Make disconnect() idempotent by forgetting the handle after closing it, and drop the redundant Drop impl on LibsqlConnection, since the inner connection already closes on drop.

Add a regression test that counts sqlite3_close_v2() calls through a cfg(test) thread-local and checks a drop closes the handle exactly once, both for a plain drop and when a statement is the last owner. It fails with 2 closes on the old code.

LibsqlConnection::drop called local::Connection::disconnect(), and then
the inner local::Connection's own Drop called disconnect() again on the
same raw handle, so every connection close ran sqlite3_close_v2() twice.
The second call reads the freed sqlite3 struct. It usually returns
SQLITE_MISUSE harmlessly, but when another thread has reused the freed
block first, sqlite walks garbage and crashes. This surfaced in libsql-js
as an intermittent access violation at worker-thread exit on Windows,
where the finalizers close many connections in a burst.

Make disconnect() idempotent by forgetting the handle after closing it,
and drop the redundant Drop impl on LibsqlConnection, since the inner
connection already closes on drop.

Add a regression test that counts sqlite3_close_v2() calls through a
cfg(test) thread-local and checks a drop closes the handle exactly once,
both for a plain drop and when a statement is the last owner. It fails
with 2 closes on the old code.
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.

1 participant