// 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";
static boolean sSimulateOomInSanitizedStacktraceForTesting;
/**
* 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) {
if (sSimulateOomInSanitizedStacktraceForTesting) {
return null;
}
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;
}
}
}