xref: /aosp_15_r20/external/jazzer-api/src/main/java/com/code_intelligence/jazzer/driver/ExceptionUtils.kt (revision 33edd6723662ea34453766bfdca85dbfdd5342b8)

<lambda>null1 // Copyright 2021 Code Intelligence GmbH
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 @file:JvmName("ExceptionUtils")
16 
17 package com.code_intelligence.jazzer.driver
18 
19 import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow
20 import com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID
21 import com.code_intelligence.jazzer.utils.Log
22 import java.lang.management.ManagementFactory
23 import java.nio.ByteBuffer
24 import java.security.MessageDigest
25 
26 private val JAZZER_PACKAGE_PREFIX = "com.code_intelligence.jazzer."
27 private val PUBLIC_JAZZER_PACKAGES = setOf("api", "replay", "sanitizers")
28 
29 private val StackTraceElement.isInternalFrame: Boolean
30     get() = if (!className.startsWith(JAZZER_PACKAGE_PREFIX)) {
31         false
32     } else {
33         val jazzerSubPackage =
34             className.substring(JAZZER_PACKAGE_PREFIX.length).split(".", limit = 2)[0]
35         jazzerSubPackage !in PUBLIC_JAZZER_PACKAGES
36     }
37 
hashnull38 private fun hash(throwable: Throwable, passToRootCause: Boolean): ByteArray =
39     MessageDigest.getInstance("SHA-256").run {
40         // It suffices to hash the stack trace of the deepest cause as the higher-level causes only
41         // contain part of the stack trace (plus possibly a different exception type).
42         var rootCause = throwable
43         if (passToRootCause) {
44             while (true) {
45                 rootCause = rootCause.cause ?: break
46             }
47         }
48         update(rootCause.javaClass.name.toByteArray())
49         rootCause.stackTrace
50             .takeWhile { !it.isInternalFrame }
51             .filterNot {
52                 it.className.startsWith("jdk.internal.") ||
53                     it.className.startsWith("java.lang.reflect.") ||
54                     it.className.startsWith("sun.reflect.") ||
55                     it.className.startsWith("java.lang.invoke.")
56             }
57             .forEach { update(it.toString().toByteArray()) }
58         if (throwable.suppressed.isNotEmpty()) {
59             update("suppressed".toByteArray())
60             for (suppressed in throwable.suppressed) {
61                 update(hash(suppressed, passToRootCause))
62             }
63         }
64         digest()
65     }
66 
67 /**
68  * Computes a hash of the stack trace of [throwable] without messages.
69  *
70  * The hash can be used to deduplicate stack traces obtained on crashes. By not including the
71  * messages, this hash should not depend on the precise crashing input.
72  */
computeDedupTokennull73 fun computeDedupToken(throwable: Throwable): Long {
74     var passToRootCause = true
75     if (throwable is FuzzerSecurityIssueLow && throwable.cause is StackOverflowError) {
76         // Special handling for StackOverflowErrors as processed by preprocessThrowable:
77         // Only consider the repeated part of the stack trace and ignore the original stack trace in
78         // the cause.
79         passToRootCause = false
80     }
81     return ByteBuffer.wrap(hash(throwable, passToRootCause)).long
82 }
83 
84 /**
85  * Annotates [throwable] with a severity and additional information if it represents a bug type
86  * that has security content.
87  */
preprocessThrowablenull88 fun preprocessThrowable(throwable: Throwable): Throwable = when (throwable) {
89     is StackOverflowError -> {
90         // StackOverflowErrors are hard to deduplicate as the top-most stack frames vary wildly,
91         // whereas the information that is most useful for deduplication detection is hidden in the
92         // rest of the (truncated) stack frame.
93         // We heuristically clean up the stack trace by taking the elements from the bottom and
94         // stopping at the first repetition of a frame. The original error is returned as the cause
95         // unchanged.
96         val observedFrames = mutableSetOf<StackTraceElement>()
97         val bottomFramesWithoutRepetition = throwable.stackTrace.takeLastWhile { frame ->
98             (frame !in observedFrames).also { observedFrames.add(frame) }
99         }
100         var securityIssueMessage = "Stack overflow"
101         if (!IS_ANDROID) {
102             securityIssueMessage = "$securityIssueMessage (use '${getReproducingXssArg()}' to reproduce)"
103         }
104         FuzzerSecurityIssueLow(securityIssueMessage, throwable).apply {
105             stackTrace = bottomFramesWithoutRepetition.toTypedArray()
106         }
107     }
108     is OutOfMemoryError -> {
109         var securityIssueMessage = "Out of memory"
110         if (!IS_ANDROID) {
111             securityIssueMessage = "$securityIssueMessage (use '${getReproducingXmxArg()}' to reproduce)"
112         }
113         stripOwnStackTrace(FuzzerSecurityIssueLow(securityIssueMessage, throwable))
114     }
115     is VirtualMachineError -> stripOwnStackTrace(FuzzerSecurityIssueLow(throwable))
116     else -> throwable
117 }.also { dropInternalFrames(it) }
118 
119 /**
120  * Recursively strips all Jazzer-internal stack frames from the given [Throwable] and its causes.
121  */
dropInternalFramesnull122 private fun dropInternalFrames(throwable: Throwable?) {
123     throwable?.run {
124         stackTrace = stackTrace.takeWhile { !it.isInternalFrame }.toTypedArray()
125         suppressed.forEach { it.stackTrace = stackTrace.takeWhile { !it.isInternalFrame }.toTypedArray() }
126         dropInternalFrames(throwable.cause)
127     }
128 }
129 
130 /**
131  * Strips the stack trace of [throwable] (e.g. because it was created in a utility method), but not
132  * the stack traces of its causes.
133  */
<lambda>null134 private fun stripOwnStackTrace(throwable: Throwable) = throwable.apply {
135     stackTrace = emptyArray()
136 }
137 
138 /**
139  * Returns a valid `-Xmx` JVM argument that sets the stack size to a value with which [StackOverflowError] findings can
140  * be reproduced, assuming the environment is sufficiently similar (e.g. OS and JVM version).
141  */
getReproducingXmxArgnull142 private fun getReproducingXmxArg(): String? {
143     val maxHeapSizeInMegaBytes = (getNumericFinalFlagValue("MaxHeapSize") ?: return null) shr 20
144     val conservativeMaxHeapSizeInMegaBytes = (maxHeapSizeInMegaBytes * 0.9).toInt()
145     return "-Xmx${conservativeMaxHeapSizeInMegaBytes}m"
146 }
147 
148 /**
149  * Returns a valid `-Xss` JVM argument that sets the stack size to a value with which [StackOverflowError] findings can
150  * be reproduced, assuming the environment is sufficiently similar (e.g. OS and JVM version).
151  */
getReproducingXssArgnull152 private fun getReproducingXssArg(): String? {
153     val threadStackSizeInKiloBytes = getNumericFinalFlagValue("ThreadStackSize") ?: return null
154     val conservativeThreadStackSizeInKiloBytes = (threadStackSizeInKiloBytes * 0.9).toInt()
155     return "-Xss${conservativeThreadStackSizeInKiloBytes}k"
156 }
157 
getNumericFinalFlagValuenull158 private fun getNumericFinalFlagValue(arg: String): Long? {
159     val argPattern = "$arg\\D*(\\d*)".toRegex()
160     return argPattern.find(javaFullFinalFlags ?: return null)?.groupValues?.get(1)?.toLongOrNull()
161 }
162 
<lambda>null163 private val javaFullFinalFlags by lazy {
164     readJavaFullFinalFlags()
165 }
166 
readJavaFullFinalFlagsnull167 private fun readJavaFullFinalFlags(): String? {
168     val javaHome = System.getProperty("java.home") ?: return null
169     val javaBinary = "$javaHome/bin/java"
170     val currentJvmArgs = ManagementFactory.getRuntimeMXBean().inputArguments
171     val javaPrintFlagsProcess = ProcessBuilder(
172         listOf(javaBinary) + currentJvmArgs + listOf(
173             "-XX:+PrintFlagsFinal",
174             "-version",
175         ),
176     ).start()
177     return javaPrintFlagsProcess.inputStream.bufferedReader().useLines { lineSequence ->
178         lineSequence
179             .filter { it.contains("ThreadStackSize") || it.contains("MaxHeapSize") }
180             .joinToString("\n")
181     }
182 }
183 
dumpAllStackTracesnull184 fun dumpAllStackTraces() {
185     Log.println("\nStack traces of all JVM threads:")
186     for ((thread, stack) in Thread.getAllStackTraces()) {
187         Log.println(thread.toString())
188         // Remove traces of this method and the methods it calls.
189         stack.asList()
190             .asReversed()
191             .takeWhile {
192                 !(
193                     it.className == "com.code_intelligence.jazzer.driver.ExceptionUtils" &&
194                         it.methodName == "dumpAllStackTraces"
195                     )
196             }
197             .asReversed()
198             .forEach { frame ->
199                 Log.println("\tat $frame")
200             }
201         Log.println("")
202     }
203 
204     if (IS_ANDROID) {
205         // ManagementFactory is not supported on Android
206         return
207     }
208 
209     Log.println("Garbage collector stats:")
210     Log.println(
211         ManagementFactory.getGarbageCollectorMXBeans().joinToString("\n", "\n", "\n") {
212             "${it.name}: ${it.collectionCount} collections took ${it.collectionTime}ms"
213         },
214     )
215 }
216