// 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.driver import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow import com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID import com.code_intelligence.jazzer.utils.Log import java.lang.management.ManagementFactory import java.nio.ByteBuffer import java.security.MessageDigest private val JAZZER_PACKAGE_PREFIX = "com.code_intelligence.jazzer." private val PUBLIC_JAZZER_PACKAGES = setOf("api", "replay", "sanitizers") private val StackTraceElement.isInternalFrame: Boolean get() = if (!className.startsWith(JAZZER_PACKAGE_PREFIX)) { false } else { val jazzerSubPackage = className.substring(JAZZER_PACKAGE_PREFIX.length).split(".", limit = 2)[0] jazzerSubPackage !in PUBLIC_JAZZER_PACKAGES } 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()) rootCause.stackTrace .takeWhile { !it.isInternalFrame } .filterNot { it.className.startsWith("jdk.internal.") || it.className.startsWith("java.lang.reflect.") || it.className.startsWith("sun.reflect.") || it.className.startsWith("java.lang.invoke.") } .forEach { update(it.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() val bottomFramesWithoutRepetition = throwable.stackTrace.takeLastWhile { frame -> (frame !in observedFrames).also { observedFrames.add(frame) } } var securityIssueMessage = "Stack overflow" if (!IS_ANDROID) { securityIssueMessage = "$securityIssueMessage (use '${getReproducingXssArg()}' to reproduce)" } FuzzerSecurityIssueLow(securityIssueMessage, throwable).apply { stackTrace = bottomFramesWithoutRepetition.toTypedArray() } } is OutOfMemoryError -> { var securityIssueMessage = "Out of memory" if (!IS_ANDROID) { securityIssueMessage = "$securityIssueMessage (use '${getReproducingXmxArg()}' to reproduce)" } stripOwnStackTrace(FuzzerSecurityIssueLow(securityIssueMessage, throwable)) } is VirtualMachineError -> stripOwnStackTrace(FuzzerSecurityIssueLow(throwable)) else -> throwable }.also { dropInternalFrames(it) } /** * Recursively strips all Jazzer-internal stack frames from the given [Throwable] and its causes. */ private fun dropInternalFrames(throwable: Throwable?) { throwable?.run { stackTrace = stackTrace.takeWhile { !it.isInternalFrame }.toTypedArray() suppressed.forEach { it.stackTrace = stackTrace.takeWhile { !it.isInternalFrame }.toTypedArray() } dropInternalFrames(throwable.cause) } } /** * 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() { Log.println("\nStack traces of all JVM threads:") for ((thread, stack) in Thread.getAllStackTraces()) { Log.println(thread.toString()) // Remove traces of this method and the methods it calls. stack.asList() .asReversed() .takeWhile { !( it.className == "com.code_intelligence.jazzer.driver.ExceptionUtils" && it.methodName == "dumpAllStackTraces" ) } .asReversed() .forEach { frame -> Log.println("\tat $frame") } Log.println("") } if (IS_ANDROID) { // ManagementFactory is not supported on Android return } Log.println("Garbage collector stats:") Log.println( ManagementFactory.getGarbageCollectorMXBeans().joinToString("\n", "\n", "\n") { "${it.name}: ${it.collectionCount} collections took ${it.collectionTime}ms" }, ) }