// CrashHandler.cpp // // Async-signal-safe crash dumper for the face_sdk native library. The goal // is that the next time the app dies (e.g. another FORTIFY: pthread_mutex_lock // called on a destroyed mutex from inside the Vulkan driver) we don't only // have logcat — we also have a self-contained dump on the device's app data // directory that the user can pull off and send back, even after a reboot. // // Why we need this: // - DebugLog only writes "happy path" events. The Vulkan crash happens // synchronously inside vkQueueSubmit / vkFreeCommandBuffers — there is // no DebugLog call near the crash site, so the file log just stops. // - logcat survives across the crash (debuggerd dumps the backtrace there) // but logcat is volatile: a couple of reboots, a logcat -c, or a long // idle period and it's gone. We want a persistent file. // - tombstones in /data/tombstones/ are root-only on consumer devices. // // Implementation notes: // - Inside a signal handler we MUST stick to async-signal-safe APIs // (man 7 signal-safety). That rules out fprintf / snprintf / malloc. // We use write(), our own integer-to-string conversion, and a single // pre-opened fd. dladdr() is technically not on the POSIX safe list but // bionic's implementation only takes one rwlock and is widely used in // other crash dumpers (breakpad, crashpad, libunwindstack). We accept // that risk because the alternative is no symbol at all. // - We use (_Unwind_Backtrace) instead of execinfo.h because // bionic doesn't ship execinfo.h on all NDK levels, and _Unwind_Backtrace // is the same primitive Android's own tombstoned uses. // - We re-raise the original signal with the default handler at the end so // the OS still produces a tombstone / ANR record for vendors that // read /data/tombstones/. #include "CrashHandler.h" #ifndef _WIN32 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { // ---------- Globals (touched from the handler -> only POD/atomics) --------- constexpr size_t kCrashStackSize = 64 * 1024; // sigaltstack constexpr size_t kMaxFrames = 64; constexpr size_t kNoteCapacity = 256; uint8_t g_sigStack[kCrashStackSize]; int g_crashFd = -1; // crash log fd (append, sync) int g_debugFd = -1; // optional: also dup write to debug log std::atomic g_installed{false}; std::atomic g_handlingCrash{false}; // Single writer of g_note: setNote() (uses memcpy under a tiny lock, but the // handler reads byte-by-byte so a torn read at most produces a truncated // note, never a deref of bad memory). char g_note[kNoteCapacity] = {0}; pthread_mutex_t g_noteMtx = PTHREAD_MUTEX_INITIALIZER; const int g_signals[] = { SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSYS }; constexpr size_t kNumSignals = sizeof(g_signals) / sizeof(g_signals[0]); // ----------------------- Async-signal-safe writers ------------------------- // Write a NUL-terminated string. Drops the trailing NUL. void sigWrite(int fd, const char* s) { if (fd < 0 || !s) return; size_t n = 0; while (s[n]) ++n; if (n == 0) return; // Loop until everything is written or we error out. Async-safe. while (n > 0) { ssize_t w = write(fd, s, n); if (w <= 0) { if (w < 0 && errno == EINTR) continue; return; } s += w; n -= (size_t)w; } } // Write n bytes from buf. Used for the note (may contain anything). void sigWriteN(int fd, const char* buf, size_t n) { if (fd < 0 || !buf) return; while (n > 0) { ssize_t w = write(fd, buf, n); if (w <= 0) { if (w < 0 && errno == EINTR) continue; return; } buf += w; n -= (size_t)w; } } // Write the same string to both the crash log and the debug log (if any). void sigDump(const char* s) { sigWrite(g_crashFd, s); sigWrite(g_debugFd, s); } // Convert an unsigned integer to its decimal representation. Returns the // number of characters written into buf (without a NUL). size_t u64ToDec(uint64_t v, char* buf, size_t cap) { if (cap == 0) return 0; char tmp[32]; size_t i = 0; if (v == 0) { tmp[i++] = '0'; } else { while (v && i < sizeof(tmp)) { tmp[i++] = (char)('0' + (v % 10)); v /= 10; } } size_t out = (i < cap) ? i : cap; for (size_t k = 0; k < out; ++k) { buf[k] = tmp[i - 1 - k]; } return out; } // Convert an unsigned 64-bit value to a fixed-width 16-digit hex string. // Useful for PC values. size_t u64ToHex16(uint64_t v, char* buf, size_t cap) { static const char digits[] = "0123456789abcdef"; if (cap < 16) return 0; for (int i = 15; i >= 0; --i) { buf[i] = digits[v & 0xF]; v >>= 4; } return 16; } // Write "=\n". void sigWriteKV_u64(int fd, const char* key, uint64_t v) { sigWrite(fd, key); sigWrite(fd, "="); char num[24]; size_t n = u64ToDec(v, num, sizeof(num)); sigWriteN(fd, num, n); sigWrite(fd, "\n"); } // Write "=0x\n". void sigWriteKV_ptr(int fd, const char* key, uint64_t v) { sigWrite(fd, key); sigWrite(fd, "=0x"); char hx[16]; u64ToHex16(v, hx, sizeof(hx)); sigWriteN(fd, hx, 16); sigWrite(fd, "\n"); } // ------------------------- Signal name lookup ------------------------------ const char* signalName(int signo) { switch (signo) { case SIGSEGV: return "SIGSEGV"; case SIGABRT: return "SIGABRT"; case SIGBUS: return "SIGBUS"; case SIGFPE: return "SIGFPE"; case SIGILL: return "SIGILL"; case SIGSYS: return "SIGSYS"; case SIGTRAP: return "SIGTRAP"; default: return "SIG?"; } } // si_code to short string. Only the most common ones; everything else falls // back to the numeric value via sigWriteKV_u64. const char* siCodeName(int signo, int code) { switch (signo) { case SIGSEGV: if (code == SEGV_MAPERR) return "SEGV_MAPERR"; if (code == SEGV_ACCERR) return "SEGV_ACCERR"; break; case SIGBUS: if (code == BUS_ADRALN) return "BUS_ADRALN"; if (code == BUS_ADRERR) return "BUS_ADRERR"; if (code == BUS_OBJERR) return "BUS_OBJERR"; break; case SIGFPE: if (code == FPE_INTDIV) return "FPE_INTDIV"; if (code == FPE_INTOVF) return "FPE_INTOVF"; if (code == FPE_FLTDIV) return "FPE_FLTDIV"; break; case SIGILL: if (code == ILL_ILLOPC) return "ILL_ILLOPC"; if (code == ILL_ILLOPN) return "ILL_ILLOPN"; break; case SIGABRT: if (code == SI_TKILL) return "SI_TKILL"; if (code == SI_USER) return "SI_USER"; break; } return "?"; } // ------------------------- Backtrace via _Unwind_Backtrace ----------------- struct UnwindCtx { uintptr_t* frames; size_t count; size_t cap; }; _Unwind_Reason_Code unwindCallback(_Unwind_Context* ctx, void* arg) { UnwindCtx* uc = static_cast(arg); if (uc->count >= uc->cap) return _URC_END_OF_STACK; uintptr_t pc = _Unwind_GetIP(ctx); if (pc) { // Trim Thumb bit on 32-bit ARM. No-op on aarch64/x86_64. pc &= ~(uintptr_t)1; uc->frames[uc->count++] = pc; } return _URC_NO_REASON; } size_t captureBacktrace(uintptr_t* out, size_t cap) { UnwindCtx uc{out, 0, cap}; _Unwind_Backtrace(&unwindCallback, &uc); return uc.count; } // Dump one frame: " #02 pc 000000000000abcd /path/lib.so (Symbol+0x10)" void dumpFrame(int fd, size_t idx, uintptr_t pc) { sigWrite(fd, " #"); char num[8]; if (idx < 10) { num[0] = '0'; num[1] = (char)('0' + idx); sigWriteN(fd, num, 2); } else { size_t n = u64ToDec(idx, num, sizeof(num)); sigWriteN(fd, num, n); } sigWrite(fd, " pc "); char hx[16]; u64ToHex16((uint64_t)pc, hx, sizeof(hx)); sigWriteN(fd, hx, 16); Dl_info info; memset(&info, 0, sizeof(info)); if (dladdr(reinterpret_cast(pc), &info) && info.dli_fname) { sigWrite(fd, " "); sigWrite(fd, info.dli_fname); if (info.dli_sname) { uintptr_t sym = reinterpret_cast(info.dli_saddr); uintptr_t off = (sym && pc >= sym) ? (pc - sym) : 0; sigWrite(fd, " ("); sigWrite(fd, info.dli_sname); sigWrite(fd, "+0x"); char ohx[16]; u64ToHex16((uint64_t)off, ohx, sizeof(ohx)); sigWriteN(fd, ohx, 16); sigWrite(fd, ")"); } else if (info.dli_fbase) { uintptr_t base = reinterpret_cast(info.dli_fbase); uintptr_t off = (pc >= base) ? (pc - base) : 0; sigWrite(fd, " (offset 0x"); char ohx[16]; u64ToHex16((uint64_t)off, ohx, sizeof(ohx)); sigWriteN(fd, ohx, 16); sigWrite(fd, ")"); } } sigWrite(fd, "\n"); } // ------------------------- Time + tid helpers ------------------------------ // Builds "YYYY-MM-DD HH:MM:SS.mmm UTC" into the given buffer (no NUL). // Returns the number of bytes written. Async-signal-safe (no stdio, no // localtime_r tz lookups). size_t formatTimestamp(char* b, size_t cap) { timespec ts{}; clock_gettime(CLOCK_REALTIME, &ts); struct tm tm_info{}; time_t s = ts.tv_sec; gmtime_r(&s, &tm_info); size_t i = 0; auto putUInt = [&](unsigned v, int width) { char tmp[8]; size_t n = u64ToDec(v, tmp, sizeof(tmp)); while ((int)n < (size_t)width) { if (i < cap) b[i++] = '0'; ++n; } for (size_t k = 0; k < n; ++k) { if (i < cap) b[i++] = tmp[k]; } }; putUInt((unsigned)(tm_info.tm_year + 1900), 4); if (i < cap) b[i++] = '-'; putUInt((unsigned)(tm_info.tm_mon + 1), 2); if (i < cap) b[i++] = '-'; putUInt((unsigned)(tm_info.tm_mday), 2); if (i < cap) b[i++] = ' '; putUInt((unsigned)(tm_info.tm_hour), 2); if (i < cap) b[i++] = ':'; putUInt((unsigned)(tm_info.tm_min), 2); if (i < cap) b[i++] = ':'; putUInt((unsigned)(tm_info.tm_sec), 2); if (i < cap) b[i++] = '.'; putUInt((unsigned)(ts.tv_nsec / 1000000), 3); static const char kSuffix[] = " UTC"; for (size_t k = 0; k < sizeof(kSuffix) - 1; ++k) { if (i < cap) b[i++] = kSuffix[k]; } return i; } pid_t currentTid() { return static_cast(syscall(SYS_gettid)); } // ------------------------- Signal handler ---------------------------------- void crashHandler(int signo, siginfo_t* info, void* ucontext) { (void)ucontext; // Re-entry guard: if a second signal fires while we're dumping (e.g. our // own dladdr trips a SIGSEGV) just chain to the default handler. bool expected = false; if (!g_handlingCrash.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) { // Already in handler -> default + bail. signal(signo, SIG_DFL); raise(signo); return; } sigDump("\n========== FACE_SDK CRASH ==========\n"); sigDump("time="); { char tsbuf[48]; size_t tn = formatTimestamp(tsbuf, sizeof(tsbuf)); sigWriteN(g_crashFd, tsbuf, tn); sigWriteN(g_debugFd, tsbuf, tn); } sigDump("\n"); sigDump("signal="); sigDump(signalName(signo)); sigDump(" code="); sigDump(siCodeName(signo, info ? info->si_code : 0)); sigDump("\n"); if (info) { sigWriteKV_u64(g_crashFd, "si_signo", (uint64_t)info->si_signo); sigWriteKV_u64(g_debugFd, "si_signo", (uint64_t)info->si_signo); sigWriteKV_u64(g_crashFd, "si_code", (uint64_t)info->si_code); sigWriteKV_u64(g_debugFd, "si_code", (uint64_t)info->si_code); sigWriteKV_ptr(g_crashFd, "si_addr", (uint64_t)(uintptr_t)info->si_addr); sigWriteKV_ptr(g_debugFd, "si_addr", (uint64_t)(uintptr_t)info->si_addr); } sigWriteKV_u64(g_crashFd, "pid", (uint64_t)getpid()); sigWriteKV_u64(g_debugFd, "pid", (uint64_t)getpid()); sigWriteKV_u64(g_crashFd, "tid", (uint64_t)currentTid()); sigWriteKV_u64(g_debugFd, "tid", (uint64_t)currentTid()); // Note (current frame index, current motion, ...). sigDump("note="); sigWriteN(g_crashFd, g_note, strnlen(g_note, kNoteCapacity)); sigWriteN(g_debugFd, g_note, strnlen(g_note, kNoteCapacity)); sigDump("\n"); sigDump("backtrace:\n"); uintptr_t frames[kMaxFrames]; size_t nf = captureBacktrace(frames, kMaxFrames); for (size_t i = 0; i < nf; ++i) { dumpFrame(g_crashFd, i, frames[i]); dumpFrame(g_debugFd, i, frames[i]); } sigDump("==================================\n"); // Make sure everything reaches disk before we self-destruct. if (g_crashFd >= 0) fsync(g_crashFd); if (g_debugFd >= 0) fsync(g_debugFd); // Mirror to logcat too so a quick `adb logcat -d` after a reboot still // shows the SIGNAL line (helps cross-checking the file). __android_log_print(ANDROID_LOG_FATAL, "FACE_DBG_CRASH", "fatal %s @ tid=%d, see face_sdk_crash.log", signalName(signo), currentTid()); // Restore default handler and re-raise. This produces /data/tombstones/* // on rooted devices and tells debuggerd to print the official Android // backtrace into logcat (`crash_dump64 ... DEBUG`). struct sigaction dfl{}; dfl.sa_handler = SIG_DFL; sigemptyset(&dfl.sa_mask); sigaction(signo, &dfl, nullptr); raise(signo); } } // namespace namespace CrashHandler { void install(const std::string& internalDataPath, const std::string& debugLogPath) { bool expected = false; if (!g_installed.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) { return; // already installed } // Open the persistent crash log file in append mode. We keep it open // forever so the signal handler doesn't have to call open() (which is // safe but slow). { std::string path = internalDataPath.empty() ? std::string("/data/local/tmp/face_sdk_crash.log") : internalDataPath + "/face_sdk_crash.log"; g_crashFd = open(path.c_str(), O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644); if (g_crashFd >= 0) { // Header for the new run. sigWrite(g_crashFd, "\n=== CrashHandler installed pid="); char pidbuf[16]; size_t n = u64ToDec((uint64_t)getpid(), pidbuf, sizeof(pidbuf)); sigWriteN(g_crashFd, pidbuf, n); sigWrite(g_crashFd, " ===\n"); fsync(g_crashFd); __android_log_print(ANDROID_LOG_INFO, "FACE_DBG", "CrashHandler log file: %s", path.c_str()); } else { __android_log_print(ANDROID_LOG_WARN, "FACE_DBG", "CrashHandler failed to open %s: %s", path.c_str(), strerror(errno)); } } // Also keep a writable fd to the DebugLog file (if any) so dumps land // next to the regular tail of the log. We don't touch g_fp inside // DebugLog because that would need its mutex — not safe in handler. if (!debugLogPath.empty()) { g_debugFd = open(debugLogPath.c_str(), O_WRONLY | O_APPEND | O_CLOEXEC, 0644); if (g_debugFd < 0) { __android_log_print(ANDROID_LOG_WARN, "FACE_DBG", "CrashHandler: cannot open debug log %s: %s", debugLogPath.c_str(), strerror(errno)); } } // Set up an alternate stack so we still have stack space if the original // thread ran out (very common for Vulkan crashes inside deep driver // call chains). stack_t ss{}; ss.ss_sp = g_sigStack; ss.ss_size = sizeof(g_sigStack); ss.ss_flags = 0; if (sigaltstack(&ss, nullptr) != 0) { __android_log_print(ANDROID_LOG_WARN, "FACE_DBG", "CrashHandler: sigaltstack failed: %s", strerror(errno)); } struct sigaction sa{}; sa.sa_sigaction = &crashHandler; sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESTART; sigemptyset(&sa.sa_mask); for (size_t i = 0; i < kNumSignals; ++i) { if (sigaction(g_signals[i], &sa, nullptr) != 0) { __android_log_print(ANDROID_LOG_WARN, "FACE_DBG", "CrashHandler: sigaction(%d) failed: %s", g_signals[i], strerror(errno)); } } __android_log_print(ANDROID_LOG_INFO, "FACE_DBG", "CrashHandler installed for SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL/SIGSYS"); } void setNote(const char* note) { if (!note) note = ""; pthread_mutex_lock(&g_noteMtx); size_t n = strnlen(note, kNoteCapacity - 1); memcpy(g_note, note, n); g_note[n] = '\0'; pthread_mutex_unlock(&g_noteMtx); } } // namespace CrashHandler #else // _WIN32 — no-op on host build namespace CrashHandler { void install(const std::string&, const std::string&) {} void setNote(const char*) {} } // namespace CrashHandler #endif