From f8c8335da33d4ff0d231f09413093b63b0bf500b Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 14 Sep 2026 04:17:27 -0500 Subject: [PATCH 1/2] Node-API (JavaScriptCore): only erase the environment registry entry we own The JavaScriptCore backend maps JSGlobalContextRef -> napi_env in a static registry so host callbacks can recover their env from the context. The env destructor erased the entry by context pointer unconditionally. Detach runs after JSGlobalContextRelease (finalizers still need the env), so between the release and the destructor a *new* environment can be handed the same context address and register it; the old destructor then erased the new environment's entry, and its next callback resolved ToNapi() to nullptr and dereferenced it (SIGSEGV inside NativeInfo::Query). With a single environment this never happens, which is why it stayed latent; it surfaced immediately once environments were created and torn down in quick succession (a worker-style create/terminate loop). Erase the entry only if it still maps to this environment. --- Core/Node-API/Source/js_native_api_javascriptcore.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.h b/Core/Node-API/Source/js_native_api_javascriptcore.h index 8d8dbd02..5d782f1e 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.h +++ b/Core/Node-API/Source/js_native_api_javascriptcore.h @@ -47,7 +47,13 @@ struct napi_env__ { deinit_symbol(function_info_symbol); deinit_symbol(constructor_info_symbol); JSGlobalContextRelease(context); - napi_envs.erase(context); + // Erase only this environment's own registration. JavaScriptCore can hand a new environment the + // address of a context that was released just before this destructor runs (Detach must follow + // JSGlobalContextRelease so finalizers can still resolve their env), and an unconditional erase + // would then drop that newer environment's entry, leaving its callbacks with ToNapi() == nullptr. + if (const auto it = napi_envs.find(context); it != napi_envs.end() && it->second == this) { + napi_envs.erase(it); + } } static napi_env get(JSGlobalContextRef context) { From 6f4e7e2ec840adffd89e40f67d2430b667a43ba9 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 14 Sep 2026 07:03:57 -0500 Subject: [PATCH 2/2] Node-API (JavaScriptCore): guard the environment registry with a mutex Environments live on their own runtime threads, so registration, lookup and the conditional erase all touch one std::unordered_map concurrently. --- Core/Node-API/Source/js_native_api_javascriptcore.h | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.h b/Core/Node-API/Source/js_native_api_javascriptcore.h index 5d782f1e..e18b33e9 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.h +++ b/Core/Node-API/Source/js_native_api_javascriptcore.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -32,7 +33,10 @@ struct napi_env__ { const std::thread::id thread_id{std::this_thread::get_id()}; napi_env__(JSGlobalContextRef context) : context{context} { - napi_envs[context] = this; + { + std::lock_guard lock{napi_envs_mutex}; + napi_envs[context] = this; + } JSGlobalContextRetain(context); init_symbol(constructor_info_symbol, "BabylonNative_ConstructorInfo"); init_symbol(function_info_symbol, "BabylonNative_FunctionInfo"); @@ -51,12 +55,14 @@ struct napi_env__ { // address of a context that was released just before this destructor runs (Detach must follow // JSGlobalContextRelease so finalizers can still resolve their env), and an unconditional erase // would then drop that newer environment's entry, leaving its callbacks with ToNapi() == nullptr. + std::lock_guard lock{napi_envs_mutex}; if (const auto it = napi_envs.find(context); it != napi_envs.end() && it->second == this) { napi_envs.erase(it); } } static napi_env get(JSGlobalContextRef context) { + std::lock_guard lock{napi_envs_mutex}; auto it = napi_envs.find(context); if (it != napi_envs.end()) { return it->second; @@ -66,6 +72,8 @@ struct napi_env__ { } private: + // Environments live on their own runtime threads, so the registry is shared between them. + static inline std::mutex napi_envs_mutex{}; static inline std::unordered_map napi_envs{}; void deinit_refs();