aboutsummaryrefslogtreecommitdiff
path: root/agent/src/main/java/com/code_intelligence/jazzer/utils
diff options
context:
space:
mode:
Diffstat (limited to 'agent/src/main/java/com/code_intelligence/jazzer/utils')
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel5
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt31
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/utils/ExceptionUtils.kt172
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt54
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt25
5 files changed, 278 insertions, 9 deletions
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel
index 5e301efc..10e3477c 100644
--- a/agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel
+++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel
@@ -4,7 +4,12 @@ kt_jvm_library(
name = "utils",
srcs = [
"ClassNameGlobber.kt",
+ "ExceptionUtils.kt",
+ "ManifestUtils.kt",
"Utils.kt",
],
visibility = ["//visibility:public"],
+ deps = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/api",
+ ],
)
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt b/agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt
index 1f09afe3..44249c81 100644
--- a/agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt
+++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt
@@ -14,28 +14,41 @@
package com.code_intelligence.jazzer.utils
-import java.lang.IllegalArgumentException
-
private val BASE_INCLUDED_CLASS_NAME_GLOBS = listOf(
"**", // everything
)
+// We use both a strong indicator for running as a Bazel test together with an indicator for a
+// Bazel coverage run to rule out false positives.
+private val IS_BAZEL_COVERAGE_RUN = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") != null &&
+ System.getenv("COVERAGE_DIR") != null
+
+private val ADDITIONAL_EXCLUDED_NAME_GLOBS_FOR_BAZEL_COVERAGE = listOf(
+ "com.google.testing.coverage.**",
+ "org.jacoco.**",
+)
+
private val BASE_EXCLUDED_CLASS_NAME_GLOBS = listOf(
+ // JDK internals
"\\[**", // array types
- "com.code_intelligence.jazzer.**",
- "com.sun.**", // package for Proxy objects
"java.**",
"javax.**",
- "jaz.Ter", // safe companion of the honeypot class used by sanitizers
- "jaz.Zer", // honeypot class used by sanitizers
"jdk.**",
- "kotlin.**",
"sun.**",
-)
+ "com.sun.**", // package for Proxy objects
+ // Azul JDK internals
+ "com.azul.tooling.**",
+ // Kotlin internals
+ "kotlin.**",
+ // Jazzer internals
+ "com.code_intelligence.jazzer.**",
+ "jaz.Ter", // safe companion of the honeypot class used by sanitizers
+ "jaz.Zer", // honeypot class used by sanitizers
+) + if (IS_BAZEL_COVERAGE_RUN) ADDITIONAL_EXCLUDED_NAME_GLOBS_FOR_BAZEL_COVERAGE else listOf()
class ClassNameGlobber(includes: List<String>, excludes: List<String>) {
// If no include globs are provided, start with all classes.
- private val includeMatchers = (if (includes.isEmpty()) BASE_INCLUDED_CLASS_NAME_GLOBS else includes)
+ private val includeMatchers = includes.ifEmpty { BASE_INCLUDED_CLASS_NAME_GLOBS }
.map(::SimpleGlobMatcher)
// If no include globs are provided, additionally exclude stdlib classes as well as our own classes.
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"
+ }
+ )
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt b/agent/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt
new file mode 100644
index 00000000..e7165e55
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt
@@ -0,0 +1,54 @@
+// 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.
+
+package com.code_intelligence.jazzer.utils
+
+import java.util.jar.Manifest
+
+object ManifestUtils {
+
+ private const val FUZZ_TARGET_CLASS = "Jazzer-Fuzz-Target-Class"
+ const val HOOK_CLASSES = "Jazzer-Hook-Classes"
+
+ fun combineManifestValues(attribute: String): List<String> {
+ val manifests = ClassLoader.getSystemResources("META-INF/MANIFEST.MF")
+ return manifests.asSequence().mapNotNull { url ->
+ url.openStream().use { inputStream ->
+ val manifest = Manifest(inputStream)
+ manifest.mainAttributes.getValue(attribute)
+ }
+ }.toList()
+ }
+
+ /**
+ * Returns the value of the `Fuzz-Target-Class` manifest attribute if there is a unique one among all manifest
+ * files in the classpath.
+ */
+ @JvmStatic
+ fun detectFuzzTargetClass(): String? {
+ val fuzzTargets = combineManifestValues(FUZZ_TARGET_CLASS)
+ return when (fuzzTargets.size) {
+ 0 -> null
+ 1 -> fuzzTargets.first()
+ else -> {
+ println(
+ """
+ |WARN: More than one Jazzer-Fuzz-Target-Class manifest entry detected on the
+ |classpath.""".trimMargin()
+ )
+ null
+ }
+ }
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt b/agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt
index af8cce9b..1b399baf 100644
--- a/agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt
+++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt
@@ -17,6 +17,8 @@ package com.code_intelligence.jazzer.utils
import java.lang.reflect.Executable
import java.lang.reflect.Method
+import java.nio.ByteBuffer
+import java.nio.channels.FileChannel
val Class<*>.descriptor: String
get() = when {
@@ -80,3 +82,26 @@ fun simpleFastHash(vararg strings: String): Int {
}
return hash
}
+
+/**
+ * Reads the [FileChannel] to the end as a UTF-8 string.
+ */
+fun FileChannel.readFully(): String {
+ check(size() <= Int.MAX_VALUE)
+ val buffer = ByteBuffer.allocate(size().toInt())
+ while (buffer.hasRemaining()) {
+ when (read(buffer)) {
+ 0 -> throw IllegalStateException("No bytes read")
+ -1 -> break
+ }
+ }
+ return String(buffer.array())
+}
+
+/**
+ * Appends [string] to the end of the [FileChannel].
+ */
+fun FileChannel.append(string: String) {
+ position(size())
+ write(ByteBuffer.wrap(string.toByteArray()))
+}