| // 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/debug/debugging_buildflags.h" |
| #include "base/debug/stack_trace.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" |
| #include "third_party/jni_zero/system_jni/Throwable_jni.h" |
| |
| // Must come after all headers that specialize FromJniType() / ToJniType(). |
| #include "base/jni_android_jni/JniAndroid_jni.h" |
| |
| namespace base { |
| namespace android { |
| namespace { |
| |
| #if !BUILDFLAG(IS_ROBOLECTRIC) |
| thread_local const JNINativeInterface* g_previous_functions = nullptr; |
| thread_local JNINativeInterface g_hooked_functions; |
| #endif |
| |
| // 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) |
| jclass FindClassHook(JNIEnv* env, const char* class_name) { |
| // * For env->FindClass(), class_name with be in the form "foo/bar/Baz". |
| // * jni_zero::GetClass() expects "foo.bar.Baz" because it uses |
| // ClassLoader.load() to load classes. |
| // * ClassLoader.load() does not work with foo/bar/Baz on JVM, nor on |
| // Android code in the boot classpath (like Cronet), but does work for |
| // normal Android apps. |
| // This hook is currently used only in Chrome for non-bootclasspath |
| // cases, so we do not bother with the / -> . translation. |
| jclass clazz = jni_zero::GetClass(env, class_name).Release(); |
| if (clazz) { |
| return clazz; |
| } |
| // ClassLoader.loadClass() throws ClassNotFoundException, but FindClass() |
| // throws only NoClassDefFoundError. |
| ClearException(env); |
| return g_previous_functions->FindClass(env, class_name); |
| } |
| #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(); |
| 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 = jni_zero::AdoptRef(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, 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) { |
| std::string sanitized_exception_string = |
| Java_JniAndroid_sanitizedStacktraceForUnhandledException(env, throwable); |
| // Returns null when PiiElider results in an OutOfMemoryError. |
| return !sanitized_exception_string.empty() |
| ? 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); |
| } |
| |
| void HookJniFindClass(JNIEnv* env) { |
| #if !BUILDFLAG(IS_ROBOLECTRIC) |
| if (g_previous_functions) { |
| return; |
| } |
| g_previous_functions = env->functions; |
| g_hooked_functions = *g_previous_functions; |
| env->functions = &g_hooked_functions; |
| g_hooked_functions.FindClass = &FindClassHook; |
| #endif |
| } |
| |
| void UnhookJniFindClassForTesting(JNIEnv* env) { |
| #if !BUILDFLAG(IS_ROBOLECTRIC) |
| if (g_previous_functions == nullptr) { |
| return; |
| } |
| env->functions = g_previous_functions; |
| g_previous_functions = nullptr; |
| g_hooked_functions = {}; |
| #endif |
| } |
| |
| #if !BUILDFLAG(IS_ROBOLECTRIC) |
| const JNINativeInterface* GetOriginalJniFunctionsForTesting() { |
| return g_previous_functions; |
| } |
| #endif |
| |
| } // namespace android |
| } // namespace base |
| |
| DEFINE_JNI(JniAndroid) |