Skip to content

[Bug]: iOS EXC_BAD_ACCESS during Hermes runtime teardown in ResultPropNames cache on 18.1.4 #446

Description

@CavalcanteLeo

What happened?

After upgrading @op-engineering/op-sqlite from 18.1.3 to 18.1.4, an Expo / React Native iOS development build crashes with EXC_BAD_ACCESS (SIGSEGV) when the Hermes runtime is replaced during a Metro reload.

The database has already been opened and queried before the reload. The crash happens while the React Native JavaScript thread exits and C++ thread-local storage is finalized.

This appears to be a regression introduced by commit 32f2f185059060e3244bcc39957e7a5746ab3ac9, which added a per-runtime thread_local std::unordered_map<jsi::Runtime *, ResultPropNames> holding three jsi::PropNameID values.

Versions

  • op-sqlite: 18.1.4, currently the latest published version
  • last known good: 18.1.3
  • React Native: 0.86.2
  • Expo: 57.0.16
  • JavaScript engine: Hermes
  • architecture: New Architecture / bridgeless development build
  • device: iPhone 16 Simulator, iOS 18.6
  • host: macOS 26.6, Apple Silicon

Reproducible example

The application repository is not public, but the failure is deterministic with this runtime sequence:

  1. Install op-sqlite 18.1.4 in a Hermes New Architecture iOS app.
  2. Open a database.
  3. Execute at least one statement that creates an execute result, so result_prop_names(rt) populates its thread-local cache.
  4. Trigger a Metro reload or otherwise replace the React Native runtime.
  5. Allow the old com.facebook.react.runtime.JavaScript thread to exit.

Observed result: the app terminates with a null-pointer EXC_BAD_ACCESS while destroying the cached jsi::PropNameID values during thread-local finalization.

Expected result: the old runtime and JavaScript thread tear down without a native crash.

Symbolicated crash excerpt

Triggered by Thread: 5  com.facebook.react.runtime.JavaScript
Exception Type:    EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000

0  facebook::jsi::Pointer::~Pointer()                         jsi.h:833
1  facebook::jsi::PropNameID::~PropNameID()                  jsi.h:848
2  facebook::jsi::PropNameID::~PropNameID()                  jsi.h:848
3  opsqlite::(anonymous namespace)::ResultPropNames::~ResultPropNames()
                                                               OPUtils.cpp:30
...
12 std::__1::unordered_map<facebook::jsi::Runtime *,
     opsqlite::ResultPropNames>::~unordered_map()
14 dyld::ThreadLocalVariables::finalizeList(void *)
16 _pthread_tsd_cleanup
17 _pthread_exit

The full symbolicated stack shows destruction of the thread-local unordered_map after the JSI runtime lifetime has ended.

Suspected cause

jsi::PropNameID is runtime-scoped. The cache owns those handles in thread-local storage:

ResultPropNames &result_prop_names(jsi::Runtime &rt) {
  static thread_local std::unordered_map<jsi::Runtime *, ResultPropNames> cache;
  // ...
}

The map key distinguishes overlapping runtime generations, but no cache entry is erased before the corresponding runtime is destroyed. When thread-local storage later finalizes, each PropNameID destructor can touch a runtime that is already gone. The null address and teardown-only stack are consistent with that lifetime mismatch.

A fix likely needs explicit cache cleanup tied to runtime invalidation, or a cache representation that does not own runtime-scoped JSI handles beyond the runtime lifetime.

Regression confirmation and mitigation

  • 18.1.4: reproducible native crash during runtime teardown.
  • Exact pin to 18.1.3 followed by a fresh native rebuild: the same database initialization and repeated runtime-reload flow survived four consecutive reloads without a crash.
  • No local package patch is being used.

Please let me know if a standalone public reproduction is still required after reviewing the symbolicated lifetime path; I can help reduce the sequence further.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions