From 7d534a0306dfd4cfc873a9d155b1c6eaef33a489 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 01:58:50 +0900 Subject: [PATCH 1/2] Release vtable and cursor handles when SQLite destroys them --- callback.go | 21 ++++++++++++++++++ sqlite3_opt_vtable.go | 7 ++++++ sqlite3_opt_vtable_leak_test.go | 38 +++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 sqlite3_opt_vtable_leak_test.go diff --git a/callback.go b/callback.go index 8ea17db..b7df2be 100644 --- a/callback.go +++ b/callback.go @@ -131,6 +131,27 @@ func lookupHandle(handle unsafe.Pointer) any { return lookupHandleVal(handle).val } +// deleteHandle releases a single handle created by newHandle. It is a no-op +// if the handle is unknown (e.g. already released). +func deleteHandle(handle unsafe.Pointer) { + handleLock.Lock() + defer handleLock.Unlock() + + current := loadHandleVals() + if _, ok := current[handle]; !ok { + return + } + next := make(map[unsafe.Pointer]handleVal, len(current)-1) + for h, v := range current { + if h == handle { + continue + } + next[h] = v + } + handleVals.Store(next) + C.free(handle) +} + func deleteHandles(db *SQLiteConn) { handleLock.Lock() defer handleLock.Unlock() diff --git a/sqlite3_opt_vtable.go b/sqlite3_opt_vtable.go index 9e916ea..d6e3ef6 100644 --- a/sqlite3_opt_vtable.go +++ b/sqlite3_opt_vtable.go @@ -423,6 +423,9 @@ func goVRelease(pVTab unsafe.Pointer, isDestroy C.int) *C.char { } else { err = vt.vTab.Disconnect() } + // The vtab is gone as far as SQLite is concerned regardless of the + // callback result, so release the handle either way. + deleteHandle(pVTab) if err != nil { return mPrintf("%s", err.Error()) } @@ -492,6 +495,9 @@ func goVBestIndex(pVTab unsafe.Pointer, icp unsafe.Pointer) *C.char { func goVClose(pCursor unsafe.Pointer) *C.char { vtc := lookupHandle(pCursor).(*sqliteVTabCursor) err := vtc.vTabCursor.Close() + // The cursor is gone as far as SQLite is concerned regardless of the + // callback result, so release the handle either way. + deleteHandle(pCursor) if err != nil { return mPrintf("%s", err.Error()) } @@ -502,6 +508,7 @@ func goVClose(pCursor unsafe.Pointer) *C.char { func goMDestroy(pClientData unsafe.Pointer) { m := lookupHandle(pClientData).(*sqliteModule) m.module.DestroyModule() + deleteHandle(pClientData) } //export goVFilter diff --git a/sqlite3_opt_vtable_leak_test.go b/sqlite3_opt_vtable_leak_test.go new file mode 100644 index 0000000..3d8b28e --- /dev/null +++ b/sqlite3_opt_vtable_leak_test.go @@ -0,0 +1,38 @@ +//go:build sqlite_vtable + +package sqlite3 + +import ( + "database/sql" + "testing" +) + +func TestVtabCursorHandleRelease(t *testing.T) { + sql.Register("sqlite3_HandleLeakCheck", &SQLiteDriver{ + ConnectHook: func(conn *SQLiteConn) error { + return conn.CreateModule("test", &testModule{t: t, intarray: []int{1, 2, 3}}) + }, + }) + db, err := sql.Open("sqlite3_HandleLeakCheck", ":memory:") + if err != nil { + t.Fatal(err) + } + defer db.Close() + if _, err := db.Exec("CREATE VIRTUAL TABLE vtab USING test('1', 2, three)"); err != nil { + t.Fatal(err) + } + var before, after int + for i := 0; i < 50; i++ { + var n int + if err := db.QueryRow("SELECT count(*) FROM vtab").Scan(&n); err != nil { + t.Fatal(err) + } + if i == 0 { + before = len(loadHandleVals()) + } + } + after = len(loadHandleVals()) + if after > before { + t.Fatalf("handle map grew from %d to %d over repeated cursor open/close", before, after) + } +} From f65452a23dce8eec5a5d841530ff0e13adc98a0d Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 10:04:39 +0900 Subject: [PATCH 2/2] Use unique driver name in leak test --- sqlite3_opt_vtable_leak_test.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sqlite3_opt_vtable_leak_test.go b/sqlite3_opt_vtable_leak_test.go index 3d8b28e..d2b7047 100644 --- a/sqlite3_opt_vtable_leak_test.go +++ b/sqlite3_opt_vtable_leak_test.go @@ -4,16 +4,23 @@ package sqlite3 import ( "database/sql" + "fmt" + "sync/atomic" "testing" ) +var leakCheckDriverSeq int32 + func TestVtabCursorHandleRelease(t *testing.T) { - sql.Register("sqlite3_HandleLeakCheck", &SQLiteDriver{ + // Use a unique driver name so repeated runs (e.g. -count=2) do not + // panic on duplicate registration. + driverName := fmt.Sprintf("sqlite3_HandleLeakCheck_%d", atomic.AddInt32(&leakCheckDriverSeq, 1)) + sql.Register(driverName, &SQLiteDriver{ ConnectHook: func(conn *SQLiteConn) error { return conn.CreateModule("test", &testModule{t: t, intarray: []int{1, 2, 3}}) }, }) - db, err := sql.Open("sqlite3_HandleLeakCheck", ":memory:") + db, err := sql.Open(driverName, ":memory:") if err != nil { t.Fatal(err) }