1 // Copyright 2023 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base; 6 7 import org.jni_zero.CalledByNative; 8 9 /** Provides Java-side code to back `jni_android` native logic. */ 10 public final class JniAndroid { JniAndroid()11 private JniAndroid() {} 12 13 private static final String TAG = "JniAndroid"; 14 15 /** 16 * Returns a sanitized stacktrace (per {@link PiiElider#sanitizeStacktrace(String)}) for the 17 * given throwable. Returns null if an OutOfMemoryError occurs. 18 * 19 * <p>Since this is running inside an uncaught exception handler, this method will make every 20 * effort not to throw; instead, any failures will be surfaced through the returned string. 21 */ 22 @CalledByNative sanitizedStacktraceForUnhandledException(Throwable throwable)23 private static String sanitizedStacktraceForUnhandledException(Throwable throwable) { 24 try { 25 return PiiElider.sanitizeStacktrace(Log.getStackTraceString(throwable)); 26 } catch (OutOfMemoryError oomError) { 27 return null; 28 } catch (Throwable stacktraceThrowable) { 29 try { 30 return "Error while getting stack trace: " 31 + Log.getStackTraceString(stacktraceThrowable); 32 } catch (OutOfMemoryError oomError) { 33 return null; 34 } 35 } 36 } 37 38 /** 39 * Indicates that native code was faced with an uncaught Java exception. 40 * 41 * <p>{@code #getCause} returns the original uncaught exception. 42 */ 43 public static class UncaughtExceptionException extends RuntimeException { UncaughtExceptionException(String nativeStackTrace, Throwable uncaughtException)44 public UncaughtExceptionException(String nativeStackTrace, Throwable uncaughtException) { 45 super( 46 "Native stack trace:" + System.lineSeparator() + nativeStackTrace, 47 uncaughtException); 48 } 49 } 50 51 /** 52 * Called by the Chromium native JNI framework when faced with an uncaught Java exception while 53 * executing a Java method from native code. 54 * 55 * <p>This method is expected to terminate the process (but is not guaranteed to). 56 * 57 * <p>The goal of this method is to provide an opportunity to terminate the process from the 58 * Java side so that the crash looks like any other uncaught Java exception, and is handled 59 * accordingly by system crash handlers. This ensures the Java stack trace will be collected, as 60 * opposed to the native stack trace - the former is typically more useful as the true root 61 * cause of the crash is Java code, not native code. See https://crbug.com/1426888 for more 62 * discussion. 63 * 64 * <p>This method will make every effort not to throw to avoid re-entering the Chromium JNI 65 * native exception handler. Errors will be sent to the system log instead. 66 * 67 * @param throwable The uncaught Java exception that was thrown by a Java method called via JNI. 68 * @param nativeStackTrace The stack trace of the native code that called the Java method that 69 * threw. 70 * @return null, unless the uncaught exception handler threw an exception other than 71 * OutOfMemoryError exception, in which case that exception is returned. 72 */ 73 @CalledByNative handleException(Throwable throwable, String nativeStackTrace)74 private static Throwable handleException(Throwable throwable, String nativeStackTrace) { 75 try { 76 // Try to make sure the exception details at least make their way to the log even if the 77 // rest of this method goes horribly wrong. 78 Log.e(TAG, "Handling uncaught Java exception", throwable); 79 80 // Wrap the original exception so that we can annotate it with native stack information, 81 // with the goal of including as much information in the Java crash report as possible. 82 // (The native caller might itself have been called from Java. We don't need to care 83 // about that because the stack trace in `throwable` includes the *entire* Java stack of 84 // the current thread, even if there are native calls in the middle.) 85 var wrappedThrowable = new UncaughtExceptionException(nativeStackTrace, throwable); 86 87 // The Chromium JNI framework does not support resuming execution after a Java method 88 // called through JNI throws an exception - we have to terminate the process at some 89 // point, otherwise undefined behavior may result. The goal here is to provide as much 90 // useful information to the crash handler as we can. 91 // 92 // To that end, we try to call the global uncaught exception handler. Hopefully that 93 // will eventually reach the default Android uncaught exception handler (possibly going 94 // through JavaExceptionReporter first, if we set one up), which will terminate the 95 // process. If for any reason that doesn't happen (e.g. the app set up a different 96 // handler), then we just give up and return the new exception (if any) - the native 97 // code we're returning to will terminate the process for us. (Note that, even then, 98 // there is still a case where we might not terminate the process: if the uncaught 99 // exception handler deliberately terminates the current thread but not the entire 100 // process. This is very contrived though, and protecting against this would be 101 // complicated, so we don't even try.) 102 Thread.getDefaultUncaughtExceptionHandler() 103 .uncaughtException(Thread.currentThread(), wrappedThrowable); 104 Log.e(TAG, "Global uncaught exception handler did not terminate the process."); 105 return null; 106 } catch (OutOfMemoryError e) { 107 // Don't call Log.e() so as to not risk throwing again. 108 return null; 109 } catch (Throwable e) { 110 // Log the new crash rather than the original crash, since if there is a bug in our 111 // crash handling logic, we need to know about it. If there is a crash in a webview 112 // app's crash handling logic, then we can rely on other apps to upload the underlying 113 // exception. 114 Log.e(TAG, "Exception in uncaught exception handler.", e); 115 return e; 116 } 117 } 118 } 119