diff --git a/anrs/anrs-impl/CMakeLists.txt b/anrs/anrs-impl/CMakeLists.txt new file mode 100644 index 000000000000..853e8fd8795f --- /dev/null +++ b/anrs/anrs-impl/CMakeLists.txt @@ -0,0 +1,29 @@ +project(crash-ndk) +cmake_minimum_required(VERSION 3.4.1) + +add_library( # Sets the name of the library. + crash-ndk + + # Sets the library as a shared library. + SHARED + + # Provides a relative path to your source file(s). + src/main/cpp/ndk-crash.cpp + src/main/cpp/jni.cpp + src/main/cpp/pixel.cpp + ) + +find_library( # Sets the name of the path variable. + log-lib + + # Specifies the name of the NDK library that + # you want CMake to locate. + log) + +target_link_libraries( + # Specifies the target library. + crash-ndk + + # Links the target library to the log library + # included in the NDK. + ${log-lib} ) \ No newline at end of file diff --git a/anrs/anrs-impl/build.gradle b/anrs/anrs-impl/build.gradle index 82ae5b9586de..176a294678ac 100644 --- a/anrs/anrs-impl/build.gradle +++ b/anrs/anrs-impl/build.gradle @@ -31,6 +31,7 @@ dependencies { implementation project(':browser-api') implementation project(':statistics') implementation project(':verified-installation-api') + implementation project(':library-loader-api') implementation AndroidX.core.ktx implementation KotlinX.coroutines.core @@ -56,8 +57,16 @@ android { anvil { generateDaggerFactories = true // default is false } - namespace 'com.duckduckgo.app.anr' + + ndkVersion '21.4.7075529' + namespace 'com.duckduckgo.app.anr' compileOptions { coreLibraryDesugaringEnabled = true } + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } } diff --git a/anrs/anrs-impl/src/main/cpp/android.h b/anrs/anrs-impl/src/main/cpp/android.h new file mode 100644 index 000000000000..69f76bb10056 --- /dev/null +++ b/anrs/anrs-impl/src/main/cpp/android.h @@ -0,0 +1,18 @@ +#ifndef ANDROID_JNI_H +#define ANDROID_JNI_H + +#include +#include + +void __platform_log_print(int prio, const char *tag, const char *fmt, ...); + +// loglevel will be assigned during library initialisation, it always has a default value +extern int loglevel; +extern char pname[256]; +extern char appVersion[256]; +extern bool isCustomTab; + +// Use this method to print log messages into the console +#define log_print(prio, format, ...) do { if (prio >= loglevel) __platform_log_print(prio, "ndk-crash", format, ##__VA_ARGS__); } while (0) + +#endif // ANDROID_JNI_H \ No newline at end of file diff --git a/anrs/anrs-impl/src/main/cpp/jni.cpp b/anrs/anrs-impl/src/main/cpp/jni.cpp new file mode 100644 index 000000000000..a5707c0a5baa --- /dev/null +++ b/anrs/anrs-impl/src/main/cpp/jni.cpp @@ -0,0 +1,146 @@ +#include +#include +#include +#include // strncpy + +#include "android.h" +#include "ndk-crash.h" +#include "pixel.h" + +/////////////////////////////////////////////////////////////////////////// + +static JavaVM *JVM = NULL; +jclass clsCrash; +jobject CLASS_JVM_CRASH = NULL; + + +static jobject jniGlobalRef(JNIEnv *env, jobject cls); +static jclass jniFindClass(JNIEnv *env, const char *name); +static jmethodID jniGetMethodID(JNIEnv *env, jclass cls, const char *name, const char *signature); + +int loglevel = 0; +char appVersion[256]; +char pname[256]; +bool isCustomTab = false; + +/////////////////////////////////////////////////////////////////////////// + + +void __platform_log_print(int prio, const char *tag, const char *fmt, ...) { + char line[1024]; + va_list argptr; + va_start(argptr, fmt); + vsprintf(line, fmt, argptr); + __android_log_print(prio, tag, "%s", line); + va_end(argptr); +} + +/////////////////////////////////////////////////////////////////////////// +// JNI utils +/////////////////////////////////////////////////////////////////////////// + +static jobject jniGlobalRef(JNIEnv *env, jobject cls) { + jobject gcls = env->NewGlobalRef(cls); + if (gcls == NULL) + log_print(ANDROID_LOG_ERROR, "Global ref failed (out of memory?)"); + return gcls; +} + +static jclass jniFindClass(JNIEnv *env, const char *name) { + jclass cls = env->FindClass(name); + if (cls == NULL) + log_print(ANDROID_LOG_ERROR, "Class %s not found", name); + return cls; +} + +static jmethodID jniGetMethodID(JNIEnv *env, jclass cls, const char *name, const char *signature) { + jmethodID method = env->GetMethodID(cls, name, signature); + if (method == NULL) { + log_print(ANDROID_LOG_ERROR, "Method %s %s not found", name, signature); + } + return method; +} + +/////////////////////////////////////////////////////////////////////////// +// JNI lifecycle +/////////////////////////////////////////////////////////////////////////// + +jint JNI_OnLoad(JavaVM *vm, void *reserved) { + JNIEnv *env; + if ((vm)->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { + log_print(ANDROID_LOG_INFO, "JNI load GetEnv failed"); + return -1; + } + + jint rs = env->GetJavaVM(&JVM); + if (rs != JNI_OK) { + log_print(ANDROID_LOG_ERROR, "Could not get JVM"); + return -1; + } + + return JNI_VERSION_1_6; +} + +void JNI_OnUnload(JavaVM *vm, void *reserved) { + log_print(ANDROID_LOG_INFO, "JNI unload"); + + JNIEnv *env; + if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) + log_print(ANDROID_LOG_INFO, "JNI load GetEnv failed"); + else { + env->DeleteGlobalRef(clsCrash); + } +} + +/////////////////////////////////////////////////////////////////////////// +// native<>JVM interface +/////////////////////////////////////////////////////////////////////////// + +extern "C" JNIEXPORT void JNICALL +Java_com_duckduckgo_app_anr_ndk_NativeCrashInit_jni_1register_1sighandler( + JNIEnv* env, + jobject instance, + jint loglevel_, + jstring version_, + jstring pname_, + jboolean customtab_ +) { + + if (!native_crash_handler_init()) { + log_print(ANDROID_LOG_ERROR, "Error initialising crash handler."); + return; + } + + // get and set loglevel + loglevel = loglevel_; + + // get and set app vesrion + const char *versionChars = env->GetStringUTFChars(version_, nullptr); + strncpy(appVersion, versionChars, sizeof(appVersion) - 1); + appVersion[sizeof(appVersion) - 1] = '\0'; // Ensure null-termination + env->ReleaseStringUTFChars(version_, versionChars); + + // get and set process name + const char *pnameChars = env->GetStringUTFChars(pname_, nullptr); + strncpy(pname, pnameChars, sizeof(pname) - 1); + pname[sizeof(pname) - 1] = '\0'; // Ensure null-termination + env->ReleaseStringUTFChars(pname_, pnameChars); + + // get and set isCustomTabs + isCustomTab = customtab_; + + clsCrash = env->GetObjectClass(instance); + const char *emptyParamVoidSig = "()V"; + CLASS_JVM_CRASH = env->NewGlobalRef(instance); + + send_crash_handle_init_pixel(); + + log_print(ANDROID_LOG_ERROR, "Native crash handler successfully initialized."); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_duckduckgo_app_anr_ndk_NativeCrashInit_jni_1unregister_sighandler( + JNIEnv* env, + jobject /* this */) { + native_crash_handler_fini(); +} diff --git a/anrs/anrs-impl/src/main/cpp/ndk-crash.cpp b/anrs/anrs-impl/src/main/cpp/ndk-crash.cpp new file mode 100644 index 000000000000..98114ff37a74 --- /dev/null +++ b/anrs/anrs-impl/src/main/cpp/ndk-crash.cpp @@ -0,0 +1,129 @@ +#include "jni.h" +#include "android.h" +#include "pixel.h" + +#include +#include +#include +#include +#include +#include + +#define sizeofa(array) sizeof(array) / sizeof(array[0]) + +// sometimes signal handlers inlinux consume crashes entirely. For those cases we trigger a signal so that we ultimately +// crash properly +#define __NR_tgkill 270 + +// Caught signals +static const int SIGNALS_TO_CATCH[] = { + SIGABRT, + SIGBUS, + SIGFPE, + SIGSEGV, + SIGILL, + SIGSTKFLT, + SIGTRAP, +}; + +// Signal handler context +struct CrashInContext { + // Old handlers of signals that we restore on de-initialization. Keep values for all possible + // signals, for unused signals nullptr value is stored. + struct sigaction old_handlers[NSIG]; +}; + +// Crash handler function signature +typedef void (*CrashSignalHandler)(int, siginfo*, void*); + +// Global instance of context. As the app can only crash once per process lifetime, this can be global +static CrashInContext* crashInContext = nullptr; + + +// Main signal handling function. +static void native_crash_sig_handler(int signo, siginfo* siginfo, void* ctxvoid) { + // Restoring an old handler to make built-in Android crash mechanism work. + sigaction(signo, &crashInContext->old_handlers[signo], nullptr); + + // Log crash message + __android_log_print(ANDROID_LOG_ERROR, "ndk-crash", "Terminating with uncaught exception of type %d", signo); + send_crash_pixel(); + + // sometimes signal handlers inlinux consume crashes entirely. For those cases we trigger a signal so that we ultimately + // crash properly, ie. to run standard bionic handler + if (siginfo->si_code <= 0 || signo == SIGABRT) { + if (syscall(__NR_tgkill, getpid(), gettid(), signo) < 0) { + _exit(1); + } + } +} + +// Register signal handler for crashes +static bool register_sig_handler(CrashSignalHandler handler, struct sigaction old_handlers[NSIG]) { + struct sigaction sigactionstruct; + memset(&sigactionstruct, 0, sizeof(sigactionstruct)); + sigactionstruct.sa_flags = SA_SIGINFO; + sigactionstruct.sa_sigaction = handler; + + // Register new handlers for all signals + for (int index = 0; index < sizeofa(SIGNALS_TO_CATCH); ++index) { + const int signo = SIGNALS_TO_CATCH[index]; + + if (sigaction(signo, &sigactionstruct, &old_handlers[signo])) { + return false; + } + } + + return true; +} + +// Unregister already register signal handler +static void unregister_sig_handler(struct sigaction old_handlers[NSIG]) { + // Recover old handler for all signals + for (int signo = 0; signo < NSIG; ++signo) { + const struct sigaction* old_handler = &old_handlers[signo]; + + if (!old_handler->sa_handler) { + continue; + } + + sigaction(signo, old_handler, nullptr); + } +} + +bool native_crash_handler_fini() { + // Check if already deinitialized + if (!crashInContext) return false; + + // Unregister signal handlers + unregister_sig_handler(crashInContext->old_handlers); + + // Free singleton crash handler context + free(crashInContext); + crashInContext = nullptr; + + log_print(ANDROID_LOG_ERROR, "Native crash handler successfully deinitialized."); + + return true; +} + +bool native_crash_handler_init() { + // Check if already initialized + if (crashInContext) { + log_print(ANDROID_LOG_INFO, "Native crash handler is already initialized."); + return false; + } + + // Initialize singleton crash handler context + crashInContext = static_cast(malloc(sizeof(CrashInContext))); + memset(crashInContext, 0, sizeof(CrashInContext)); + + // Trying to register signal handler. + if (!register_sig_handler(&native_crash_sig_handler, crashInContext->old_handlers)) { + native_crash_handler_fini(); + log_print(ANDROID_LOG_ERROR, "Native crash handler initialization failed."); + return false; + } + + return true; +} diff --git a/anrs/anrs-impl/src/main/cpp/ndk-crash.h b/anrs/anrs-impl/src/main/cpp/ndk-crash.h new file mode 100644 index 000000000000..33175c5b84b3 --- /dev/null +++ b/anrs/anrs-impl/src/main/cpp/ndk-crash.h @@ -0,0 +1,10 @@ +// Java +#ifndef NDK_CRASH_H +#define NDK_CRASH_H + +// Call this method to register native crash handling +bool native_crash_handler_init(); +// Call this method to de-register native crash handling +bool native_crash_handler_fini(); + +#endif // NDK_CRASH_H \ No newline at end of file diff --git a/anrs/anrs-impl/src/main/cpp/pixel.cpp b/anrs/anrs-impl/src/main/cpp/pixel.cpp new file mode 100644 index 000000000000..4fa1c5104bdb --- /dev/null +++ b/anrs/anrs-impl/src/main/cpp/pixel.cpp @@ -0,0 +1,70 @@ +#include "android.h" + +#include +#include +#include +#include +#include +#include +#include + +#define BUFFER_SIZE 1024 + +static void send_request(const char* host, const char* path) { + // Create socket + int sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd < 0) { + log_print(ANDROID_LOG_ERROR, "Error opening socket"); + return; + } + + // Resolve host name + struct hostent *server = gethostbyname(host); + if (server == NULL) { + log_print(ANDROID_LOG_ERROR, "Error resolving host"); + close(sockfd); + return; + } + + // Fill in the address structure + struct sockaddr_in server_addr; + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + bcopy((char *)server->h_addr, (char *)&server_addr.sin_addr.s_addr, server->h_length); + server_addr.sin_port = htons(80); // HTTP port + + // Connect to server + if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { + log_print(ANDROID_LOG_ERROR, "Error connecting to server"); + close(sockfd); + return; + } + + // Send HTTP GET request + char request[BUFFER_SIZE]; + snprintf(request, BUFFER_SIZE, "GET %s HTTP/1.1\r\nHost: %s\r\n\r\n", path, host); + if (write(sockfd, request, strlen(request)) < 0) { + log_print(ANDROID_LOG_ERROR, "Error writing to socket"); + close(sockfd); + return; + } + + // Close socket + close(sockfd); +} + +void send_crash_pixel() { + const char* host = "improving.duckduckgo.com"; + char path[2048]; + sprintf(path, "/t/m_app_native_crash_android?appVersion=%s&pn=%s&customTab=%s", appVersion, pname, isCustomTab ? "true" : "false"); + send_request(host, path); + log_print(ANDROID_LOG_ERROR, "Native crash pixel sent"); +} + +void send_crash_handle_init_pixel() { + const char* host = "improving.duckduckgo.com"; + char path[2048]; + sprintf(path, "/t/m_app_register_native_crash_handler_android?appVersion=%s&pn=%s&customTab=%s", appVersion, pname, isCustomTab ? "true" : "false"); + send_request(host, path); + log_print(ANDROID_LOG_ERROR, "Native crash handler init pixel sent"); +} diff --git a/anrs/anrs-impl/src/main/cpp/pixel.h b/anrs/anrs-impl/src/main/cpp/pixel.h new file mode 100644 index 000000000000..f7bebe803f43 --- /dev/null +++ b/anrs/anrs-impl/src/main/cpp/pixel.h @@ -0,0 +1,10 @@ +// Java +#ifndef NDK_PIXEL_H +#define NDK_PIXEL_H + +// Call this method to send the native crash pixel +void send_crash_pixel(); +// Call this method to send the native crash handler init pixel +void send_crash_handle_init_pixel(); + +#endif // NDK_PIXEL_H \ No newline at end of file diff --git a/anrs/anrs-impl/src/main/java/com/duckduckgo/app/anr/CrashOfflinePixelSender.kt b/anrs/anrs-impl/src/main/java/com/duckduckgo/app/anr/CrashOfflinePixelSender.kt index 3c6d2fb4a8c6..0b5907af1a1a 100644 --- a/anrs/anrs-impl/src/main/java/com/duckduckgo/app/anr/CrashOfflinePixelSender.kt +++ b/anrs/anrs-impl/src/main/java/com/duckduckgo/app/anr/CrashOfflinePixelSender.kt @@ -84,7 +84,7 @@ class CrashOfflinePixelSender @Inject constructor( companion object { private const val EXCEPTION_SHORT_NAME = "sn" private const val EXCEPTION_MESSAGE = "m" - private const val EXCEPTION_PROCESS_NAME = "pn" + internal const val EXCEPTION_PROCESS_NAME = "pn" private const val EXCEPTION_STACK_TRACE = "ss" private const val EXCEPTION_APP_VERSION = "v" private const val EXCEPTION_TIMESTAMP = "t" diff --git a/anrs/anrs-impl/src/main/java/com/duckduckgo/app/anr/ndk/NativeCrashInit.kt b/anrs/anrs-impl/src/main/java/com/duckduckgo/app/anr/ndk/NativeCrashInit.kt new file mode 100644 index 000000000000..d06c8e4d8bbc --- /dev/null +++ b/anrs/anrs-impl/src/main/java/com/duckduckgo/app/anr/ndk/NativeCrashInit.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.anr.ndk + +import android.content.Context +import android.util.Log +import androidx.lifecycle.LifecycleOwner +import com.duckduckgo.app.browser.customtabs.CustomTabDetector +import com.duckduckgo.app.di.IsMainProcess +import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver +import com.duckduckgo.appbuildconfig.api.AppBuildConfig +import com.duckduckgo.appbuildconfig.api.isInternalBuild +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.library.loader.LibraryLoader +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import logcat.LogPriority.ERROR +import logcat.asLog +import logcat.logcat + +@ContributesMultibinding( + scope = AppScope::class, + boundType = MainProcessLifecycleObserver::class, +) +@SingleInstanceIn(AppScope::class) +class NativeCrashInit @Inject constructor( + context: Context, + @IsMainProcess private val isMainProcess: Boolean, + private val customTabDetector: CustomTabDetector, + private val appBuildConfig: AppBuildConfig, +) : MainProcessLifecycleObserver { + + private val isCustomTab: Boolean by lazy { customTabDetector.isCustomTab() } + private val processName: String by lazy { if (isMainProcess) "main" else "vpn" } + + init { + try { + LibraryLoader.loadLibrary(context, "crash-ndk") + } catch (ignored: Throwable) { + logcat(ERROR) { "ndk-crash: Error loading crash-ndk lib: ${ignored.asLog()}" } + } + } + + private external fun jni_register_sighandler(logLevel: Int, appVersion: String, processName: String, isCustomTab: Boolean) + + override fun onCreate(owner: LifecycleOwner) { + if (isMainProcess) { + jniRegisterNativeSignalHandler() + } else { + logcat(ERROR) { "ndk-crash: onCreate wrongly called in a secondary process" } + } + } + + private fun jniRegisterNativeSignalHandler() { + runCatching { + val logLevel = if (appBuildConfig.isDebug || appBuildConfig.isInternalBuild()) { + Log.VERBOSE + } else { + Log.ASSERT + } + jni_register_sighandler(logLevel, appBuildConfig.versionName, processName, isCustomTab) + }.onFailure { + logcat(ERROR) { "ndk-crash: Error calling jni_register_sighandler: ${it.asLog()}" } + } + } +}