// 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. * *

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. * *

{@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. * *

This method is expected to terminate the process (but is not guaranteed to). * *

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. * *

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; } } }