aboutsummaryrefslogtreecommitdiff
path: root/agent/src/main/java/com/code_intelligence/jazzer/utils/ExceptionUtils.kt
diff options
context:
space:
mode:
Diffstat (limited to 'agent/src/main/java/com/code_intelligence/jazzer/utils/ExceptionUtils.kt')
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/utils/ExceptionUtils.kt172
1 files changed, 172 insertions, 0 deletions
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/utils/ExceptionUtils.kt b/agent/src/main/java/com/code_intelligence/jazzer/utils/ExceptionUtils.kt
new file mode 100644
index 00000000..30f6fb30
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/ExceptionUtils.kt
@@ -0,0 +1,172 @@
+// Copyright 2021 Code Intelligence GmbH
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@file:JvmName("ExceptionUtils")
+
+package com.code_intelligence.jazzer.utils
+
+import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow
+import java.lang.management.ManagementFactory
+import java.nio.ByteBuffer
+import java.security.MessageDigest
+
+private fun hash(throwable: Throwable, passToRootCause: Boolean): ByteArray =
+ MessageDigest.getInstance("SHA-256").run {
+ // It suffices to hash the stack trace of the deepest cause as the higher-level causes only
+ // contain part of the stack trace (plus possibly a different exception type).
+ var rootCause = throwable
+ if (passToRootCause) {
+ while (true) {
+ rootCause = rootCause.cause ?: break
+ }
+ }
+ update(rootCause.javaClass.name.toByteArray())
+ for (element in rootCause.stackTrace) {
+ update(element.toString().toByteArray())
+ }
+ if (throwable.suppressed.isNotEmpty()) {
+ update("suppressed".toByteArray())
+ for (suppressed in throwable.suppressed) {
+ update(hash(suppressed, passToRootCause))
+ }
+ }
+ digest()
+ }
+
+/**
+ * Computes a hash of the stack trace of [throwable] without messages.
+ *
+ * The hash can be used to deduplicate stack traces obtained on crashes. By not including the
+ * messages, this hash should not depend on the precise crashing input.
+ */
+fun computeDedupToken(throwable: Throwable): Long {
+ var passToRootCause = true
+ if (throwable is FuzzerSecurityIssueLow && throwable.cause is StackOverflowError) {
+ // Special handling for StackOverflowErrors as processed by preprocessThrowable:
+ // Only consider the repeated part of the stack trace and ignore the original stack trace in
+ // the cause.
+ passToRootCause = false
+ }
+ return ByteBuffer.wrap(hash(throwable, passToRootCause)).long
+}
+
+/**
+ * Annotates [throwable] with a severity and additional information if it represents a bug type
+ * that has security content.
+ */
+fun preprocessThrowable(throwable: Throwable): Throwable = when (throwable) {
+ is StackOverflowError -> {
+ // StackOverflowErrors are hard to deduplicate as the top-most stack frames vary wildly,
+ // whereas the information that is most useful for deduplication detection is hidden in the
+ // rest of the (truncated) stack frame.
+ // We heuristically clean up the stack trace by taking the elements from the bottom and
+ // stopping at the first repetition of a frame. The original error is returned as the cause
+ // unchanged.
+ val observedFrames = mutableSetOf<StackTraceElement>()
+ val bottomFramesWithoutRepetition = throwable.stackTrace.takeLastWhile { frame ->
+ (frame !in observedFrames).also { observedFrames.add(frame) }
+ }
+ FuzzerSecurityIssueLow("Stack overflow (use '${getReproducingXssArg()}' to reproduce)", throwable).apply {
+ stackTrace = bottomFramesWithoutRepetition.toTypedArray()
+ }
+ }
+ is OutOfMemoryError -> stripOwnStackTrace(
+ FuzzerSecurityIssueLow(
+ "Out of memory (use '${getReproducingXmxArg()}' to reproduce)", throwable
+ )
+ )
+ is VirtualMachineError -> stripOwnStackTrace(FuzzerSecurityIssueLow(throwable))
+ else -> throwable
+}
+
+/**
+ * Strips the stack trace of [throwable] (e.g. because it was created in a utility method), but not
+ * the stack traces of its causes.
+ */
+private fun stripOwnStackTrace(throwable: Throwable) = throwable.apply {
+ stackTrace = emptyArray()
+}
+
+/**
+ * Returns a valid `-Xmx` JVM argument that sets the stack size to a value with which [StackOverflowError] findings can
+ * be reproduced, assuming the environment is sufficiently similar (e.g. OS and JVM version).
+ */
+private fun getReproducingXmxArg(): String? {
+ val maxHeapSizeInMegaBytes = (getNumericFinalFlagValue("MaxHeapSize") ?: return null) shr 20
+ val conservativeMaxHeapSizeInMegaBytes = (maxHeapSizeInMegaBytes * 0.9).toInt()
+ return "-Xmx${conservativeMaxHeapSizeInMegaBytes}m"
+}
+
+/**
+ * Returns a valid `-Xss` JVM argument that sets the stack size to a value with which [StackOverflowError] findings can
+ * be reproduced, assuming the environment is sufficiently similar (e.g. OS and JVM version).
+ */
+private fun getReproducingXssArg(): String? {
+ val threadStackSizeInKiloBytes = getNumericFinalFlagValue("ThreadStackSize") ?: return null
+ val conservativeThreadStackSizeInKiloBytes = (threadStackSizeInKiloBytes * 0.9).toInt()
+ return "-Xss${conservativeThreadStackSizeInKiloBytes}k"
+}
+
+private fun getNumericFinalFlagValue(arg: String): Long? {
+ val argPattern = "$arg\\D*(\\d*)".toRegex()
+ return argPattern.find(javaFullFinalFlags ?: return null)?.groupValues?.get(1)?.toLongOrNull()
+}
+
+private val javaFullFinalFlags by lazy {
+ readJavaFullFinalFlags()
+}
+
+private fun readJavaFullFinalFlags(): String? {
+ val javaHome = System.getProperty("java.home") ?: return null
+ val javaBinary = "$javaHome/bin/java"
+ val currentJvmArgs = ManagementFactory.getRuntimeMXBean().inputArguments
+ val javaPrintFlagsProcess = ProcessBuilder(
+ listOf(javaBinary) + currentJvmArgs + listOf(
+ "-XX:+PrintFlagsFinal",
+ "-version"
+ )
+ ).start()
+ return javaPrintFlagsProcess.inputStream.bufferedReader().useLines { lineSequence ->
+ lineSequence
+ .filter { it.contains("ThreadStackSize") || it.contains("MaxHeapSize") }
+ .joinToString("\n")
+ }
+}
+
+fun dumpAllStackTraces() {
+ System.err.println("\nStack traces of all JVM threads:\n")
+ for ((thread, stack) in Thread.getAllStackTraces()) {
+ System.err.println(thread)
+ // Remove traces of this method and the methods it calls.
+ stack.asList()
+ .asReversed()
+ .takeWhile {
+ !(
+ it.className == "com.code_intelligence.jazzer.runtime.ExceptionUtils" &&
+ it.methodName == "dumpAllStackTraces"
+ )
+ }
+ .asReversed()
+ .forEach { frame ->
+ System.err.println("\tat $frame")
+ }
+ System.err.println()
+ }
+ System.err.println("Garbage collector stats:")
+ System.err.println(
+ ManagementFactory.getGarbageCollectorMXBeans().joinToString("\n", "\n", "\n") {
+ "${it.name}: ${it.collectionCount} collections took ${it.collectionTime}ms"
+ }
+ )
+}