mirror of
https://github.com/klzgrad/naiveproxy.git
synced 2024-11-25 06:46:09 +03:00
273 lines
11 KiB
C++
273 lines
11 KiB
C++
// Copyright 2012 The Chromium Authors
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "base/android/jni_android.h"
|
|
|
|
#include <stddef.h>
|
|
#include <sys/prctl.h>
|
|
|
|
#include "base/android/java_exception_reporter.h"
|
|
#include "base/android/jni_string.h"
|
|
#include "base/android/jni_utils.h"
|
|
#include "base/android_runtime_jni_headers/Throwable_jni.h"
|
|
#include "base/debug/debugging_buildflags.h"
|
|
#include "base/feature_list.h"
|
|
#include "base/logging.h"
|
|
#include "base/strings/string_util.h"
|
|
#include "build/build_config.h"
|
|
#include "build/robolectric_buildflags.h"
|
|
#include "third_party/jni_zero/jni_zero.h"
|
|
|
|
#if BUILDFLAG(IS_ROBOLECTRIC)
|
|
#include "base/base_robolectric_jni/JniAndroid_jni.h" // nogncheck
|
|
#else
|
|
#include "base/base_jni/JniAndroid_jni.h"
|
|
#endif
|
|
|
|
namespace base {
|
|
namespace android {
|
|
namespace {
|
|
|
|
// If disabled, we LOG(FATAL) immediately in native code when faced with an
|
|
// uncaught Java exception (historical behavior). If enabled, we give the Java
|
|
// uncaught exception handler a chance to handle the exception first, so that
|
|
// the crash is (hopefully) seen as a Java crash, not a native crash.
|
|
// TODO(crbug.com/40261529): remove this switch once we are confident the
|
|
// new behavior is fine.
|
|
BASE_FEATURE(kHandleExceptionsInJava,
|
|
"HandleJniExceptionsInJava",
|
|
base::FEATURE_ENABLED_BY_DEFAULT);
|
|
|
|
jclass g_out_of_memory_error_class = nullptr;
|
|
|
|
#if !BUILDFLAG(IS_ROBOLECTRIC)
|
|
jmethodID g_class_loader_load_class_method_id = nullptr;
|
|
// ClassLoader.loadClass() accepts either slashes or dots on Android, but JVM
|
|
// requires dots. We could translate, but there is no need to go through
|
|
// ClassLoaders in Robolectric anyways.
|
|
// https://cs.android.com/search?q=symbol:DexFile_defineClassNative
|
|
jclass GetClassFromSplit(JNIEnv* env,
|
|
const char* class_name,
|
|
const char* split_name) {
|
|
DCHECK(IsStringASCII(class_name));
|
|
ScopedJavaLocalRef<jstring> j_class_name(env, env->NewStringUTF(class_name));
|
|
return static_cast<jclass>(env->CallObjectMethod(
|
|
GetSplitClassLoader(env, split_name), g_class_loader_load_class_method_id,
|
|
j_class_name.obj()));
|
|
}
|
|
|
|
// Must be called before using GetClassFromSplit - we need to set the global,
|
|
// and we need to call GetClassLoader at least once to allow the default
|
|
// resolver (env->FindClass()) to get our main ClassLoader class instance, which
|
|
// we then cache use for all future calls to GetSplitClassLoader.
|
|
void PrepareClassLoaders(JNIEnv* env) {
|
|
if (g_class_loader_load_class_method_id == nullptr) {
|
|
GetClassLoader(env);
|
|
ScopedJavaLocalRef<jclass> class_loader_clazz = ScopedJavaLocalRef<jclass>(
|
|
env, env->FindClass("java/lang/ClassLoader"));
|
|
CHECK(!ClearException(env));
|
|
g_class_loader_load_class_method_id =
|
|
env->GetMethodID(class_loader_clazz.obj(), "loadClass",
|
|
"(Ljava/lang/String;)Ljava/lang/Class;");
|
|
CHECK(!ClearException(env));
|
|
}
|
|
}
|
|
#endif // !BUILDFLAG(IS_ROBOLECTRIC)
|
|
} // namespace
|
|
|
|
LogFatalCallback g_log_fatal_callback_for_testing = nullptr;
|
|
const char kUnableToGetStackTraceMessage[] =
|
|
"Unable to retrieve Java caller stack trace as the exception handler is "
|
|
"being re-entered";
|
|
const char kReetrantOutOfMemoryMessage[] =
|
|
"While handling an uncaught Java exception, an OutOfMemoryError "
|
|
"occurred.";
|
|
const char kReetrantExceptionMessage[] =
|
|
"While handling an uncaught Java exception, another exception "
|
|
"occurred.";
|
|
const char kUncaughtExceptionMessage[] =
|
|
"Uncaught Java exception in native code. Please include the Java exception "
|
|
"stack from the Android log in your crash report.";
|
|
const char kUncaughtExceptionHandlerFailedMessage[] =
|
|
"Uncaught Java exception in native code and the Java uncaught exception "
|
|
"handler did not terminate the process. Please include the Java exception "
|
|
"stack from the Android log in your crash report.";
|
|
const char kOomInGetJavaExceptionInfoMessage[] =
|
|
"Unable to obtain Java stack trace due to OutOfMemoryError";
|
|
|
|
void InitVM(JavaVM* vm) {
|
|
jni_zero::InitVM(vm);
|
|
jni_zero::SetExceptionHandler(CheckException);
|
|
JNIEnv* env = jni_zero::AttachCurrentThread();
|
|
#if !BUILDFLAG(IS_ROBOLECTRIC)
|
|
// Warm-up needed for GetClassFromSplit, must be called before we set the
|
|
// resolver, since GetClassFromSplit won't work until after
|
|
// PrepareClassLoaders has happened.
|
|
PrepareClassLoaders(env);
|
|
jni_zero::SetClassResolver(GetClassFromSplit);
|
|
#endif
|
|
g_out_of_memory_error_class = static_cast<jclass>(
|
|
env->NewGlobalRef(env->FindClass("java/lang/OutOfMemoryError")));
|
|
DCHECK(g_out_of_memory_error_class);
|
|
}
|
|
|
|
|
|
void CheckException(JNIEnv* env) {
|
|
if (!jni_zero::HasException(env)) {
|
|
return;
|
|
}
|
|
|
|
static thread_local bool g_reentering = false;
|
|
if (g_reentering) {
|
|
// We were handling an uncaught Java exception already, but one of the Java
|
|
// methods we called below threw another exception. (This is unlikely to
|
|
// happen, as we are careful to never throw from these methods, but we
|
|
// can't rule it out entirely. E.g. an OutOfMemoryError when constructing
|
|
// the jstring for the return value of
|
|
// sanitizedStacktraceForUnhandledException().
|
|
env->ExceptionDescribe();
|
|
jthrowable raw_throwable = env->ExceptionOccurred();
|
|
env->ExceptionClear();
|
|
jclass clazz = env->GetObjectClass(raw_throwable);
|
|
bool is_oom_error = env->IsSameObject(clazz, g_out_of_memory_error_class);
|
|
env->Throw(raw_throwable); // Ensure we don't re-enter Java.
|
|
|
|
if (is_oom_error) {
|
|
base::android::SetJavaException(kReetrantOutOfMemoryMessage);
|
|
// Use different LOG(FATAL) statements to ensure unique stack traces.
|
|
if (g_log_fatal_callback_for_testing) {
|
|
g_log_fatal_callback_for_testing(kReetrantOutOfMemoryMessage);
|
|
} else {
|
|
LOG(FATAL) << kReetrantOutOfMemoryMessage;
|
|
}
|
|
} else {
|
|
base::android::SetJavaException(kReetrantExceptionMessage);
|
|
if (g_log_fatal_callback_for_testing) {
|
|
g_log_fatal_callback_for_testing(kReetrantExceptionMessage);
|
|
} else {
|
|
LOG(FATAL) << kReetrantExceptionMessage;
|
|
}
|
|
}
|
|
// Needed for tests, which do not terminate from LOG(FATAL).
|
|
return;
|
|
}
|
|
g_reentering = true;
|
|
|
|
// Log a message to ensure there is something in the log even if the rest of
|
|
// this function goes horribly wrong, and also to provide a convenient marker
|
|
// in the log for where Java exception crash information starts.
|
|
LOG(ERROR) << "Crashing due to uncaught Java exception";
|
|
|
|
const bool handle_exception_in_java =
|
|
base::FeatureList::IsEnabled(kHandleExceptionsInJava);
|
|
|
|
if (!handle_exception_in_java) {
|
|
env->ExceptionDescribe();
|
|
}
|
|
|
|
// We cannot use `ScopedJavaLocalRef` directly because that ends up calling
|
|
// env->GetObjectRefType() when DCHECK is on, and that call is not allowed
|
|
// with a pending exception according to the JNI spec.
|
|
jthrowable raw_throwable = env->ExceptionOccurred();
|
|
// Now that we saved the reference to the throwable, clear the exception.
|
|
//
|
|
// We need to do this as early as possible to remove the risk that code below
|
|
// might accidentally call back into Java, which is not allowed when `env`
|
|
// has an exception set, per the JNI spec. (For example, LOG(FATAL) doesn't
|
|
// work with a JNI exception set, because it calls
|
|
// GetJavaStackTraceIfPresent()).
|
|
env->ExceptionClear();
|
|
// The reference returned by `ExceptionOccurred()` is a local reference.
|
|
// `ExceptionClear()` merely removes the exception information from `env`;
|
|
// it doesn't delete the reference, which is why this call is valid.
|
|
auto throwable = ScopedJavaLocalRef<jthrowable>::Adopt(env, raw_throwable);
|
|
|
|
if (!handle_exception_in_java) {
|
|
base::android::SetJavaException(
|
|
GetJavaExceptionInfo(env, throwable).c_str());
|
|
if (g_log_fatal_callback_for_testing) {
|
|
g_log_fatal_callback_for_testing(kUncaughtExceptionMessage);
|
|
} else {
|
|
LOG(FATAL) << kUncaughtExceptionMessage;
|
|
}
|
|
// Needed for tests, which do not terminate from LOG(FATAL).
|
|
g_reentering = false;
|
|
return;
|
|
}
|
|
|
|
// We don't need to call SetJavaException() in this branch because we
|
|
// expect handleException() to eventually call JavaExceptionReporter through
|
|
// the global uncaught exception handler.
|
|
|
|
const std::string native_stack_trace = base::debug::StackTrace().ToString();
|
|
LOG(ERROR) << "Native stack trace:" << std::endl << native_stack_trace;
|
|
|
|
ScopedJavaLocalRef<jthrowable> secondary_exception =
|
|
Java_JniAndroid_handleException(
|
|
env, throwable, ConvertUTF8ToJavaString(env, native_stack_trace));
|
|
|
|
// Ideally handleException() should have terminated the process and we should
|
|
// not get here. This can happen in the case of OutOfMemoryError or if the
|
|
// app that embedded WebView installed an exception handler that does not
|
|
// terminate, or itself threw an exception. We cannot be confident that
|
|
// JavaExceptionReporter ran, so set the java exception explicitly.
|
|
base::android::SetJavaException(
|
|
GetJavaExceptionInfo(
|
|
env, secondary_exception ? secondary_exception : throwable)
|
|
.c_str());
|
|
if (g_log_fatal_callback_for_testing) {
|
|
g_log_fatal_callback_for_testing(kUncaughtExceptionHandlerFailedMessage);
|
|
} else {
|
|
LOG(FATAL) << kUncaughtExceptionHandlerFailedMessage;
|
|
}
|
|
// Needed for tests, which do not terminate from LOG(FATAL).
|
|
g_reentering = false;
|
|
}
|
|
|
|
std::string GetJavaExceptionInfo(JNIEnv* env,
|
|
const JavaRef<jthrowable>& throwable) {
|
|
ScopedJavaLocalRef<jstring> sanitized_exception_string =
|
|
Java_JniAndroid_sanitizedStacktraceForUnhandledException(env, throwable);
|
|
// Returns null when PiiElider results in an OutOfMemoryError.
|
|
return sanitized_exception_string
|
|
? ConvertJavaStringToUTF8(sanitized_exception_string)
|
|
: kOomInGetJavaExceptionInfoMessage;
|
|
}
|
|
|
|
std::string GetJavaStackTraceIfPresent() {
|
|
JNIEnv* env = nullptr;
|
|
JavaVM* jvm = jni_zero::GetVM();
|
|
if (jvm) {
|
|
jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_2);
|
|
}
|
|
if (!env) {
|
|
// JNI has not been initialized on this thread.
|
|
return {};
|
|
}
|
|
|
|
if (HasException(env)) {
|
|
// This can happen if CheckException() is being re-entered, decided to
|
|
// LOG(FATAL) immediately, and LOG(FATAL) itself is calling us. In that case
|
|
// it is imperative that we don't try to call Java again.
|
|
return kUnableToGetStackTraceMessage;
|
|
}
|
|
|
|
ScopedJavaLocalRef<jthrowable> throwable =
|
|
JNI_Throwable::Java_Throwable_Constructor(env);
|
|
std::string ret = GetJavaExceptionInfo(env, throwable);
|
|
// Strip the exception message and leave only the "at" lines. Example:
|
|
// java.lang.Throwable:
|
|
// {tab}at Clazz.method(Clazz.java:111)
|
|
// {tab}at ...
|
|
size_t newline_idx = ret.find('\n');
|
|
if (newline_idx == std::string::npos) {
|
|
// There are no java frames.
|
|
return {};
|
|
}
|
|
return ret.substr(newline_idx + 1);
|
|
}
|
|
|
|
} // namespace android
|
|
} // namespace base
|