Skip to content

@qvac/translation-nmtcpp@2.0.2: JsLogger JS-ref lifecycle crashes on Bare runtime teardown and on re-setLogger #1709

@mattmc-745

Description

@mattmc-745

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_globalv8::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)

  1. 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.
  2. JsInterface::setLogger should tolerate a null / uninitialized previous slot without invoking releaseJsRefs, or use a safer ref-replacement pattern robust to repeated calls.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions