Summary
Two distinct native crashes in qvac_lib_inference_addon_cpp::logger::JsLogger, both triggered by benign JS activity (MLS message decrypt → translation call path) in a multi-worklet Bare runtime host. Both are latent until a multi-isolate Bare host shifts teardown timing or replaces a logger slot.
Environment
- Package:
@qvac/translation-nmtcpp@2.0.2
- Native:
libqvac__translation-nmtcpp.2.0.2.so
- Runtime:
react-native-bare-kit@0.12.3 on Android arm64, bundle built via bare-pack --linked
- Host: three concurrent Bare worklet isolates (MLS + Hyperdrive + Lightning/WDK)
Crash 1 — SIGABRT during bare_runtime_teardown (asyncCallback against torn-down isolate)
#08 libbare-kit.so v8::Isolate::GetCurrentContext()+24
#09 libbare-kit.so js_get_global+20
#10 libqvac__translation-nmtcpp.2.0.2.so JsLogger::asyncCallback(uv_async_s*)+248
#11–#13 libbare-kit.so uv_run+424
#14 libbare-kit.so bare_runtime_teardown+284
A pending uv_async_t for a JsLogger callback is drained by uv_run during teardown. The callback executes js_get_global → v8::Isolate::GetCurrentContext() against an already-tearing isolate. C++ throw → terminate handler also throws → abort.
Crash 2 — SIGTRAP on second setLogger / releaseJsRefs V8 debug check
#00 libbare-kit.so v8::internal::GlobalHandles::NodeSpace<...>::Release(...)+248
#01 libbare-kit.so js_delete_reference+28
#02 libqvac__translation-nmtcpp.2.0.2.so JsLogger::releaseJsRefs(js_env_s*, js_ref_s*)+32
#03 libqvac__translation-nmtcpp.2.0.2.so JsLogger::setLogger(js_env_s*, js_callback_info_s*)+408
#04 libqvac__translation-nmtcpp.2.0.2.so JsInterface::setLogger(js_env_s*, js_callback_info_s*)+36
binding.setLogger is a global single-slot. A second setLogger call tries to release the previous logger's js_ref_s* before storing the new ref. On Bare/V8 the release hits a GlobalHandles::NodeSpace::Release debug breakpoint — stale/bad handle on the previous logger slot.
Why this surfaces in react-native-bare-kit hosts
TranslationInterface constructor calls binding.setLogger whenever transitionCb && typeof transitionCb === 'object' is true. TranslationNmtcpp wraps logger: null (or undefined) into new QvacLogger(null) — an OFF-mode wrapper whose _log calls are no-ops. That wrapper is still an object, so setLogger fires — even when the caller explicitly passes no logger.
In multi-isolate hosts the wire-up is pure cost: no logs observable, but uv_async handles and js_ref_s* allocations accumulate and require disciplined teardown that native code does not currently perform.
Suggested fixes (in order of preference)
JsLogger::asyncCallback should guard against a tearing isolate before calling js_get_global — likely via a liveness check on the JS env, or by cancelling the uv_async_t during the JS env finalizer rather than draining it in uv_run.
JsInterface::setLogger should tolerate a null / uninitialized previous slot without invoking releaseJsRefs, or use a safer ref-replacement pattern robust to repeated calls.
TranslationNmtcpp JS wrapper could pass null instead of an OFF-mode QvacLogger when no user logger is provided, so TranslationInterface's existing if (transitionCb && typeof transitionCb === 'object') guard short-circuits and binding.setLogger is never called.
App-layer workaround currently in use (derivable from QVAC's own source)
Set model.logger = null after new TranslationNmtcpp({...}) and before model.load(). TranslationInterface's object-guard short-circuits, binding.setLogger is never called, and both crashes are eliminated. The OFF-mode QvacLogger was producing no output anyway, so there is no observable feature loss.
Additional context
Discovered during VibeSquad's Lightning phase two-device validation (2026-04-22). Full mechanism analysis, backtraces, and SC matrix are available on request if useful for triage. Happy to turn any of the suggested fixes into a PR if a direction is preferred.
Summary
Two distinct native crashes in
qvac_lib_inference_addon_cpp::logger::JsLogger, both triggered by benign JS activity (MLS message decrypt → translation call path) in a multi-worklet Bare runtime host. Both are latent until a multi-isolate Bare host shifts teardown timing or replaces a logger slot.Environment
@qvac/translation-nmtcpp@2.0.2libqvac__translation-nmtcpp.2.0.2.soreact-native-bare-kit@0.12.3on Android arm64, bundle built viabare-pack --linkedCrash 1 — SIGABRT during
bare_runtime_teardown(asyncCallback against torn-down isolate)A pending
uv_async_tfor aJsLoggercallback is drained byuv_runduring teardown. The callback executesjs_get_global→v8::Isolate::GetCurrentContext()against an already-tearing isolate. C++ throw → terminate handler also throws → abort.Crash 2 — SIGTRAP on second
setLogger/releaseJsRefsV8 debug checkbinding.setLoggeris a global single-slot. A secondsetLoggercall tries to release the previous logger'sjs_ref_s*before storing the new ref. On Bare/V8 the release hits aGlobalHandles::NodeSpace::Releasedebug breakpoint — stale/bad handle on the previous logger slot.Why this surfaces in
react-native-bare-kithostsTranslationInterfaceconstructor callsbinding.setLoggerwhenevertransitionCb && typeof transitionCb === 'object'is true.TranslationNmtcppwrapslogger: null(or undefined) intonew QvacLogger(null)— an OFF-mode wrapper whose_logcalls are no-ops. That wrapper is still an object, sosetLoggerfires — even when the caller explicitly passes no logger.In multi-isolate hosts the wire-up is pure cost: no logs observable, but
uv_asynchandles andjs_ref_s*allocations accumulate and require disciplined teardown that native code does not currently perform.Suggested fixes (in order of preference)
JsLogger::asyncCallbackshould guard against a tearing isolate before callingjs_get_global— likely via a liveness check on the JS env, or by cancelling theuv_async_tduring the JS env finalizer rather than draining it inuv_run.JsInterface::setLoggershould tolerate a null / uninitialized previous slot without invokingreleaseJsRefs, or use a safer ref-replacement pattern robust to repeated calls.TranslationNmtcppJS wrapper could passnullinstead of an OFF-modeQvacLoggerwhen no user logger is provided, soTranslationInterface's existingif (transitionCb && typeof transitionCb === 'object')guard short-circuits andbinding.setLoggeris never called.App-layer workaround currently in use (derivable from QVAC's own source)
Set
model.logger = nullafternew TranslationNmtcpp({...})and beforemodel.load().TranslationInterface's object-guard short-circuits,binding.setLoggeris never called, and both crashes are eliminated. The OFF-modeQvacLoggerwas producing no output anyway, so there is no observable feature loss.Additional context
Discovered during VibeSquad's Lightning phase two-device validation (2026-04-22). Full mechanism analysis, backtraces, and SC matrix are available on request if useful for triage. Happy to turn any of the suggested fixes into a PR if a direction is preferred.