-
Notifications
You must be signed in to change notification settings - Fork 947
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
10 changed files
with
504 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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} ) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
#ifndef ANDROID_JNI_H | ||
#define ANDROID_JNI_H | ||
|
||
#include <cstdio> | ||
#include <android/log.h> | ||
|
||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
#include <jni.h> | ||
#include <android/log.h> | ||
#include <exception> | ||
#include <string.h> // 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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
#include "jni.h" | ||
#include "android.h" | ||
#include "pixel.h" | ||
|
||
#include <csignal> | ||
#include <cstdio> | ||
#include <cstring> | ||
#include <memory> | ||
#include <cxxabi.h> | ||
#include <unistd.h> | ||
|
||
#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<CrashInContext *>(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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
#include "android.h" | ||
|
||
#include <stdio.h> | ||
#include <stdlib.h> | ||
#include <string.h> | ||
#include <unistd.h> | ||
#include <netinet/in.h> | ||
#include <arpa/inet.h> | ||
#include <netdb.h> | ||
|
||
#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"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
anrs/anrs-impl/src/main/java/com/duckduckgo/app/anr/ndk/NativeCrashInit.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()}" } | ||
} | ||
} | ||
} |