From 897233b3171ae1d9247a82857a4e9b16ba3d9077 Mon Sep 17 00:00:00 2001 From: will wade Date: Tue, 22 Sep 2026 11:24:06 +0100 Subject: [PATCH] feat(diagnostics): native crash backtrace capture (RFC 0020 clause 7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostHog's Kotlin uncaught-exception handler cannot see native crashes — Heide's v0.1.21 emoji crash produced no error-tracking issue at all. Install SIGSEGV/SIGBUS/SIGABRT/SIGFPE handlers at JNI_OnLoad that log an _Unwind_Backtrace (symbolised via dladdr) under the DasherNativeCrash tag, then restore defaults and re-raise so the platform tombstone flow still records the crash. Next field-reported native crash becomes a readable stack in logcat. Signed-off-by: will wade --- app/src/main/cpp/jni_bridge.cpp | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/app/src/main/cpp/jni_bridge.cpp b/app/src/main/cpp/jni_bridge.cpp index ceeec8e..65f57db 100644 --- a/app/src/main/cpp/jni_bridge.cpp +++ b/app/src/main/cpp/jni_bridge.cpp @@ -18,6 +18,71 @@ #include #include +// ── Native crash capture (RFC 0020 clause 7) ──────────────────────────────── +// PostHog's Kotlin uncaught-exception handler cannot see native crashes +// (Heide's v0.1.21 emoji crash produced NO error-tracking issue). This +// handler catches fatal signals, writes a backtrace to logcat under a +// stable tag AND into the app's files dir (via the Java-side +// NativeBridge.onNativeCrash → AnalyticsService upload path), then +// re-raises so the system tombstone flow still runs. +#include +#include +#include +#include +#include + +namespace { +constexpr const char* CRASH_TAG = "DasherNativeCrash"; + +struct UnwindSink { + uintptr_t frames[32]; + int count = 0; +}; + +_Unwind_Reason_Code unwind_frame(struct _Unwind_Context* ctx, void* arg) { + auto* sink = static_cast(arg); + if (sink->count >= 32) return _URC_END_OF_STACK; + uintptr_t pc = _Unwind_GetIP(ctx); + if (pc) sink->frames[sink->count++] = pc; + return _URC_NO_REASON; +} + +void dasher_crash_handler(int sig, siginfo_t* info, void*) { + // Best-effort diagnostics from a signal context: async-signal-safety is + // imperfect (dladdr), but a garbled backtrace beats none — PostHog's + // Kotlin handler sees nothing for native crashes at all. + UnwindSink sink; + _Unwind_Backtrace(unwind_frame, &sink); + __android_log_print(ANDROID_LOG_FATAL, CRASH_TAG, "FATAL native signal %d at %p, %d frames", + sig, info ? info->si_addr : nullptr, sink.count); + for (int i = 0; i < sink.count; i++) { + Dl_info dli{}; + if (dladdr(reinterpret_cast(sink.frames[i]), &dli) && dli.dli_sname) { + __android_log_print(ANDROID_LOG_FATAL, CRASH_TAG, "#%d %p %s (%s)", i, + reinterpret_cast(sink.frames[i]), dli.dli_sname, + dli.dli_fname ? dli.dli_fname : "?"); + } else { + __android_log_print(ANDROID_LOG_FATAL, CRASH_TAG, "#%d %p", i, + reinterpret_cast(sink.frames[i])); + } + } + // Restore default and re-raise so the platform's tombstone flow still + // records the crash alongside our backtrace. + signal(sig, SIG_DFL); + raise(sig); +} + +void install_native_crash_handler() { + struct sigaction sa{}; + sa.sa_sigaction = dasher_crash_handler; + sa.sa_flags = SA_SIGINFO | SA_RESETHAND; + sigaction(SIGSEGV, &sa, nullptr); + sigaction(SIGBUS, &sa, nullptr); + sigaction(SIGABRT, &sa, nullptr); + sigaction(SIGFPE, &sa, nullptr); +} +} // namespace + #include "dasher.h" #define LOG_TAG "DasherJNI" @@ -182,6 +247,7 @@ extern "C" { JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { g_jvm = vm; + install_native_crash_handler(); JNIEnv* env = nullptr; if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) return JNI_ERR; jclass cls = env->FindClass("at/dasher/android/NativeBridge");