Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Native crash handling
Browse files Browse the repository at this point in the history
aitorvs committed May 8, 2024
1 parent 2b13d54 commit 3c64082
Showing 10 changed files with 504 additions and 2 deletions.
29 changes: 29 additions & 0 deletions anrs/anrs-impl/CMakeLists.txt
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} )
11 changes: 10 additions & 1 deletion anrs/anrs-impl/build.gradle
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
18 changes: 18 additions & 0 deletions anrs/anrs-impl/src/main/cpp/android.h
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
146 changes: 146 additions & 0 deletions anrs/anrs-impl/src/main/cpp/jni.cpp
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();
}
129 changes: 129 additions & 0 deletions anrs/anrs-impl/src/main/cpp/ndk-crash.cpp
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;
}
10 changes: 10 additions & 0 deletions anrs/anrs-impl/src/main/cpp/ndk-crash.h
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
70 changes: 70 additions & 0 deletions anrs/anrs-impl/src/main/cpp/pixel.cpp
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");
}
10 changes: 10 additions & 0 deletions anrs/anrs-impl/src/main/cpp/pixel.h
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
Original file line number Diff line number Diff line change
@@ -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"
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()}" }
}
}
}

0 comments on commit 3c64082

Please sign in to comment.