<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