Skip to content

fix: drain thread pool before closing db handle to prevent use-after-free#385

Open
isaacrowntree wants to merge 1 commit intoOP-Engineering:mainfrom
isaacrowntree:fix/drain-threadpool-before-close
Open

fix: drain thread pool before closing db handle to prevent use-after-free#385
isaacrowntree wants to merge 1 commit intoOP-Engineering:mainfrom
isaacrowntree:fix/drain-threadpool-before-close

Conversation

@isaacrowntree
Copy link

Problem

close() and invalidate() (destructor) both call opsqlite_close(db) without waiting for in-flight async queries on the thread pool to finish. If an execute() is queued or running when the db handle is freed, the worker thread dereferences a dangling sqlite3* pointer → heap corruption → SIGABRT.

Crash stack (iOS)

Thread 35 (op-sqlite thread pool worker):
  sqlite3VdbeMemSetStr
  sqlite3ErrorWithMsg
  opsqlite_execute
  ThreadPool::doWork          ← still running query
  
Thread 29 (React Native teardown):
  ReactInstance::~ReactInstance
  DBHostObject::~DBHostObject
  DBHostObject::invalidate
  opsqlite_close              ← frees db handle while Thread 35 uses it

Reproduction

This is reliably reproducible on iOS during React Native Fast Refresh. The old ReactInstance is destroyed on one thread (freeing DBHostObject and its sqlite3* handle via the destructor) while the op-sqlite thread pool worker on another thread is still executing a queued opsqlite_execute() against the now-freed handle.

The symptom at the JS level is intermittent "out of memory" errors from SQLite queries after Fast Refresh, requiring a hard app restart to recover.

Fix

close() (JS function, line 237)

Added thread_pool->waitFinished() before opsqlite_close(db) to drain any queued/running async queries. Also set db = nullptr after close for safety.

invalidate() (destructor path, line 668)

Replaced restartPool() with waitFinished(). restartPool() joins all threads (waiting for in-flight work) but then needlessly recreates the pool — wasteful during teardown. waitFinished() blocks until the queue is empty and no worker is busy, then the ThreadPool destructor (via shared_ptr release) handles thread cleanup.

Changes

 // close() JS function
 function_map["close"] = HFN(this) {
     invalidated = true;
+    thread_pool->waitFinished();
     opsqlite_close(db);
+    db = nullptr;
     return {};
 });

 // invalidate() called by destructor
 void DBHostObject::invalidate() {
     if (invalidated) return;
     invalidated = true;
-    thread_pool->restartPool();
+    thread_pool->waitFinished();
     if (db != nullptr) {
         opsqlite_close(db);
         db = nullptr;
     }
 }

Why waitFinished() is safe here

waitFinished() (already exists in OPThreadPool.h) blocks on the condition variable until workQueue.empty() && busy == 0. Since invalidated is set to true before the wait, no new JS-initiated queries can be queued (the JSI functions check invalidated before calling promisify). So the wait is bounded by the current in-flight query's execution time.

…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 <noreply@anthropic.com>
@ospfranco
Copy link
Contributor

nice, thanks for the PR

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.

2 participants