119 lines
6.1 KiB
Java
119 lines
6.1 KiB
Java
// Copyright 2023 The Chromium Authors
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
package org.chromium.base;
|
|
|
|
import org.jni_zero.CalledByNative;
|
|
|
|
/** Provides Java-side code to back `jni_android` native logic. */
|
|
public final class JniAndroid {
|
|
private JniAndroid() {}
|
|
|
|
private static final String TAG = "JniAndroid";
|
|
|
|
/**
|
|
* Returns a sanitized stacktrace (per {@link PiiElider#sanitizeStacktrace(String)}) for the
|
|
* given throwable. Returns null if an OutOfMemoryError occurs.
|
|
*
|
|
* <p>Since this is running inside an uncaught exception handler, this method will make every
|
|
* effort not to throw; instead, any failures will be surfaced through the returned string.
|
|
*/
|
|
@CalledByNative
|
|
private static String sanitizedStacktraceForUnhandledException(Throwable throwable) {
|
|
try {
|
|
return PiiElider.sanitizeStacktrace(Log.getStackTraceString(throwable));
|
|
} catch (OutOfMemoryError oomError) {
|
|
return null;
|
|
} catch (Throwable stacktraceThrowable) {
|
|
try {
|
|
return "Error while getting stack trace: "
|
|
+ Log.getStackTraceString(stacktraceThrowable);
|
|
} catch (OutOfMemoryError oomError) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indicates that native code was faced with an uncaught Java exception.
|
|
*
|
|
* <p>{@code #getCause} returns the original uncaught exception.
|
|
*/
|
|
public static class UncaughtExceptionException extends RuntimeException {
|
|
public UncaughtExceptionException(String nativeStackTrace, Throwable uncaughtException) {
|
|
super(
|
|
"Native stack trace:" + System.lineSeparator() + nativeStackTrace,
|
|
uncaughtException);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called by the Chromium native JNI framework when faced with an uncaught Java exception while
|
|
* executing a Java method from native code.
|
|
*
|
|
* <p>This method is expected to terminate the process (but is not guaranteed to).
|
|
*
|
|
* <p>The goal of this method is to provide an opportunity to terminate the process from the
|
|
* Java side so that the crash looks like any other uncaught Java exception, and is handled
|
|
* accordingly by system crash handlers. This ensures the Java stack trace will be collected, as
|
|
* opposed to the native stack trace - the former is typically more useful as the true root
|
|
* cause of the crash is Java code, not native code. See https://crbug.com/1426888 for more
|
|
* discussion.
|
|
*
|
|
* <p>This method will make every effort not to throw to avoid re-entering the Chromium JNI
|
|
* native exception handler. Errors will be sent to the system log instead.
|
|
*
|
|
* @param throwable The uncaught Java exception that was thrown by a Java method called via JNI.
|
|
* @param nativeStackTrace The stack trace of the native code that called the Java method that
|
|
* threw.
|
|
* @return null, unless the uncaught exception handler threw an exception other than
|
|
* OutOfMemoryError exception, in which case that exception is returned.
|
|
*/
|
|
@CalledByNative
|
|
private static Throwable handleException(Throwable throwable, String nativeStackTrace) {
|
|
try {
|
|
// Try to make sure the exception details at least make their way to the log even if the
|
|
// rest of this method goes horribly wrong.
|
|
Log.e(TAG, "Handling uncaught Java exception", throwable);
|
|
|
|
// Wrap the original exception so that we can annotate it with native stack information,
|
|
// with the goal of including as much information in the Java crash report as possible.
|
|
// (The native caller might itself have been called from Java. We don't need to care
|
|
// about that because the stack trace in `throwable` includes the *entire* Java stack of
|
|
// the current thread, even if there are native calls in the middle.)
|
|
var wrappedThrowable = new UncaughtExceptionException(nativeStackTrace, throwable);
|
|
|
|
// The Chromium JNI framework does not support resuming execution after a Java method
|
|
// called through JNI throws an exception - we have to terminate the process at some
|
|
// point, otherwise undefined behavior may result. The goal here is to provide as much
|
|
// useful information to the crash handler as we can.
|
|
//
|
|
// To that end, we try to call the global uncaught exception handler. Hopefully that
|
|
// will eventually reach the default Android uncaught exception handler (possibly going
|
|
// through JavaExceptionReporter first, if we set one up), which will terminate the
|
|
// process. If for any reason that doesn't happen (e.g. the app set up a different
|
|
// handler), then we just give up and return the new exception (if any) - the native
|
|
// code we're returning to will terminate the process for us. (Note that, even then,
|
|
// there is still a case where we might not terminate the process: if the uncaught
|
|
// exception handler deliberately terminates the current thread but not the entire
|
|
// process. This is very contrived though, and protecting against this would be
|
|
// complicated, so we don't even try.)
|
|
Thread.getDefaultUncaughtExceptionHandler()
|
|
.uncaughtException(Thread.currentThread(), wrappedThrowable);
|
|
Log.e(TAG, "Global uncaught exception handler did not terminate the process.");
|
|
return null;
|
|
} catch (OutOfMemoryError e) {
|
|
// Don't call Log.e() so as to not risk throwing again.
|
|
return null;
|
|
} catch (Throwable e) {
|
|
// Log the new crash rather than the original crash, since if there is a bug in our
|
|
// crash handling logic, we need to know about it. If there is a crash in a webview
|
|
// app's crash handling logic, then we can rely on other apps to upload the underlying
|
|
// exception.
|
|
Log.e(TAG, "Exception in uncaught exception handler.", e);
|
|
return e;
|
|
}
|
|
}
|
|
}
|