From df4bca43b91a6886edd8dcdfd5e62895a4db9c55 Mon Sep 17 00:00:00 2001 From: Isaac Rowntree Date: Fri, 27 Feb 2026 16:40:37 +1100 Subject: [PATCH] fix: drain thread pool before closing db handle to prevent use-after-free The `close()` JS function and `invalidate()` destructor path both call `opsqlite_close(db)` without first waiting for in-flight async queries on the thread pool to complete. If an `execute()` is queued or running when the db handle is freed, the worker thread dereferences a dangling `sqlite3*` pointer, causing heap corruption (SIGABRT in `sqlite3VdbeMemSetStr` / `_szone_free`). This is reproducible on iOS during React Native Fast Refresh: the old `ReactInstance` is destroyed (freeing `DBHostObject` via the destructor) while the thread pool still has pending queries from the previous JS context. Changes: - `close()`: call `thread_pool->waitFinished()` before `opsqlite_close()` to drain any queued/running async queries. Also set `db = nullptr` after close for safety. - `invalidate()`: replace `restartPool()` with `waitFinished()`. `restartPool()` joins threads then needlessly recreates the pool. `waitFinished()` blocks until the queue is empty and no worker is busy, then the `ThreadPool` destructor (via `shared_ptr` release) handles thread cleanup. Co-Authored-By: Claude Opus 4.6 --- cpp/DBHostObject.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cpp/DBHostObject.cpp b/cpp/DBHostObject.cpp index 96829412..ff89bcd8 100644 --- a/cpp/DBHostObject.cpp +++ b/cpp/DBHostObject.cpp @@ -237,10 +237,16 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { function_map["close"] = HFN(this) { invalidated = true; + // Drain any in-flight async queries before closing the db handle. + // Without this, a queued/running execute() on the thread pool may + // dereference the freed sqlite3* pointer → heap corruption / SIGABRT. + thread_pool->waitFinished(); + #ifdef OP_SQLITE_USE_LIBSQL opsqlite_libsql_close(db); #else opsqlite_close(db); + db = nullptr; #endif return {}; @@ -671,7 +677,14 @@ void DBHostObject::invalidate() { } invalidated = true; - thread_pool->restartPool(); + + // Drain in-flight thread pool work before closing the db handle. + // restartPool() joins threads (waiting for the current task) but then + // needlessly re-creates the pool. waitFinished() is sufficient: it + // blocks until the queue is empty and no worker is busy, then the + // ThreadPool destructor (via shared_ptr release) joins the threads. + thread_pool->waitFinished(); + #ifdef OP_SQLITE_USE_LIBSQL opsqlite_libsql_close(db); #else