aboutsummaryrefslogtreecommitdiff
path: root/agent/src
diff options
context:
space:
mode:
authorMuhammad Haseeb Ahmad <mhahmad@google.com>2021-12-30 18:15:58 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2021-12-30 18:15:58 +0000
commit0f73d9c5add52fa24500a9ddb691528db216e096 (patch)
treec5880647e8b29782d15be0c99a60e56fed6f8a02 /agent/src
parentb997679abe998d84ad4b9c3e6589342794d3bfcb (diff)
parent844d7aba71788e3f59411187316eed26ba25c7bd (diff)
downloadjazzer-api-0f73d9c5add52fa24500a9ddb691528db216e096.tar.gz
Merge remote-tracking branch 'aosp/upstream-main' into master am: 5c6f411699 am: 844d7aba71
Original change: https://android-review.googlesource.com/c/platform/external/jazzer-api/+/1935188 Change-Id: I7eec862181be56696b4cb92bb1225a6948accaf9
Diffstat (limited to 'agent/src')
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt160
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel15
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt201
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt182
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/AutofuzzConstructionException.java32
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/AutofuzzInvocationException.java26
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel28
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/CannedFuzzedDataProvider.java211
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/Consumer1.java22
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/Consumer2.java22
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/Consumer3.java20
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/Consumer4.java20
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/Consumer5.java20
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/Function1.java22
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/Function2.java22
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/Function3.java20
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/Function4.java20
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/Function5.java20
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/FuzzedDataProvider.java444
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java39
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java39
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueLow.java39
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java39
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/HookType.java24
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java499
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java183
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/api/MethodHooks.java31
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzCodegenVisitor.java117
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzError.java31
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel17
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java267
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java619
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/autofuzz/YourAverageJavaClass.java229
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/generated/BUILD.bazel40
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/generated/NoThrowDoclet.java215
-rwxr-xr-xagent/src/main/java/com/code_intelligence/jazzer/generated/update_java_no_throw_methods_list.sh18
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel45
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt53
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt229
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt90
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt35
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt201
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt119
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt48
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt386
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt45
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt258
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/instrumentor/shade_rules1
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel18
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java159
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel48
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java33
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/runtime/ExceptionUtils.kt166
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImpl.java83
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/runtime/HardToCatchError.java82
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java29
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/runtime/ManifestUtils.kt54
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/runtime/NativeLibHooks.java35
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProvider.java74
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/runtime/SignalHandler.java26
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java330
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java91
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDivHooks.java47
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceIndirHooks.java35
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel10
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt102
-rw-r--r--agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt82
-rw-r--r--agent/src/main/native/com/code_intelligence/jazzer/replay/BUILD.bazel13
-rw-r--r--agent/src/main/native/com/code_intelligence/jazzer/replay/com_code_intelligence_jazzer_replay_Replayer.cpp48
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java107
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel21
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel69
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/autofuzz/BuilderPatternTest.java103
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/autofuzz/InterfaceCreationTest.java111
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java147
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/autofuzz/SettersTest.java43
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.java85
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata/BUILD.bazel5
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata/EmployeeWithSetters.java56
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooks.java87
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt63
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTarget.java85
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTargetContract.java29
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel147
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooks.java53
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt63
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTarget.java61
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTargetContract.java25
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationSpecialCasesTarget.java41
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTarget.java67
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt141
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtilsTest.kt73
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/DynamicTestContract.java21
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt38
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java61
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java45
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockTraceDataFlowCallbacks.java106
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt53
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java109
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt63
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java120
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTargetContract.java23
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java152
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt145
-rw-r--r--agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java49
105 files changed, 9695 insertions, 0 deletions
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt b/agent/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt
new file mode 100644
index 00000000..33d02263
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt
@@ -0,0 +1,160 @@
+// 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("Agent")
+
+package com.code_intelligence.jazzer.agent
+
+import com.code_intelligence.jazzer.instrumentor.CoverageRecorder
+import com.code_intelligence.jazzer.instrumentor.InstrumentationType
+import com.code_intelligence.jazzer.instrumentor.loadHooks
+import com.code_intelligence.jazzer.runtime.ManifestUtils
+import com.code_intelligence.jazzer.utils.ClassNameGlobber
+import java.io.File
+import java.lang.instrument.Instrumentation
+import java.nio.file.Paths
+import java.util.jar.JarFile
+import kotlin.io.path.ExperimentalPathApi
+import kotlin.io.path.exists
+import kotlin.io.path.isDirectory
+
+val KNOWN_ARGUMENTS = listOf(
+ "instrumentation_includes",
+ "instrumentation_excludes",
+ "custom_hook_includes",
+ "custom_hook_excludes",
+ "trace",
+ "custom_hooks",
+ "id_sync_file",
+ "dump_classes_dir",
+)
+
+private object AgentJarFinder {
+ private val agentJarPath = AgentJarFinder::class.java.protectionDomain?.codeSource?.location?.toURI()
+ val agentJarFile = agentJarPath?.let { JarFile(File(it)) }
+}
+
+private val argumentDelimiter = if (System.getProperty("os.name").startsWith("Windows")) ";" else ":"
+
+@OptIn(ExperimentalPathApi::class)
+fun premain(agentArgs: String?, instrumentation: Instrumentation) {
+ // Add the agent jar (i.e., the jar out of which we are currently executing) to the search path of the bootstrap
+ // class loader to ensure that instrumented classes can find the CoverageMap class regardless of which ClassLoader
+ // they are using.
+ if (AgentJarFinder.agentJarFile != null) {
+ instrumentation.appendToBootstrapClassLoaderSearch(AgentJarFinder.agentJarFile)
+ } else {
+ println("WARN: Failed to add agent JAR to bootstrap class loader search path")
+ }
+ val argumentMap = (agentArgs ?: "")
+ .split(',')
+ .mapNotNull {
+ val splitArg = it.split('=', limit = 2)
+ when {
+ splitArg.size != 2 -> {
+ if (splitArg[0].isNotEmpty())
+ println("WARN: Ignoring argument ${splitArg[0]} without value")
+ null
+ }
+ splitArg[0] !in KNOWN_ARGUMENTS -> {
+ println("WARN: Ignoring unknown argument ${splitArg[0]}")
+ null
+ }
+ else -> splitArg[0] to splitArg[1].split(argumentDelimiter)
+ }
+ }.toMap()
+ val manifestCustomHookNames = ManifestUtils.combineManifestValues(ManifestUtils.HOOK_CLASSES).flatMap {
+ it.split(':')
+ }
+ val customHookNames = manifestCustomHookNames + (argumentMap["custom_hooks"] ?: emptyList())
+ val classNameGlobber = ClassNameGlobber(
+ argumentMap["instrumentation_includes"] ?: emptyList(),
+ (argumentMap["instrumentation_excludes"] ?: emptyList()) + customHookNames
+ )
+ CoverageRecorder.classNameGlobber = classNameGlobber
+ val dependencyClassNameGlobber = ClassNameGlobber(
+ argumentMap["custom_hook_includes"] ?: emptyList(),
+ (argumentMap["custom_hook_excludes"] ?: emptyList()) + customHookNames
+ )
+ val instrumentationTypes = (argumentMap["trace"] ?: listOf("all")).flatMap {
+ when (it) {
+ "cmp" -> setOf(InstrumentationType.CMP)
+ "cov" -> setOf(InstrumentationType.COV)
+ "div" -> setOf(InstrumentationType.DIV)
+ "gep" -> setOf(InstrumentationType.GEP)
+ "indir" -> setOf(InstrumentationType.INDIR)
+ "native" -> setOf(InstrumentationType.NATIVE)
+ // Disable GEP instrumentation by default as it appears to negatively affect fuzzing
+ // performance. Our current GEP instrumentation only reports constant indices, but even
+ // when we instead reported non-constant indices, they tended to completely fill up the
+ // table of recent compares and value profile map.
+ "all" -> InstrumentationType.values().toSet() - InstrumentationType.GEP
+ else -> {
+ println("WARN: Skipping unknown instrumentation type $it")
+ emptySet()
+ }
+ }
+ }.toSet()
+ val idSyncFile = argumentMap["id_sync_file"]?.let {
+ Paths.get(it.single()).also { path ->
+ println("INFO: Synchronizing coverage IDs in ${path.toAbsolutePath()}")
+ }
+ }
+ val dumpClassesDir = argumentMap["dump_classes_dir"]?.let {
+ Paths.get(it.single()).toAbsolutePath().also { path ->
+ if (path.exists() && path.isDirectory()) {
+ println("INFO: Dumping instrumented classes into $path")
+ } else {
+ println("ERROR: Cannot dump instrumented classes into $path; does not exist or not a directory")
+ }
+ }
+ }
+ val runtimeInstrumentor = RuntimeInstrumentor(
+ instrumentation,
+ classNameGlobber,
+ dependencyClassNameGlobber,
+ instrumentationTypes,
+ idSyncFile,
+ dumpClassesDir,
+ )
+ instrumentation.apply {
+ addTransformer(runtimeInstrumentor)
+ }
+
+ val relevantClassesLoadedBeforeCustomHooks = instrumentation.allLoadedClasses
+ .map { it.name }
+ .filter { classNameGlobber.includes(it) || dependencyClassNameGlobber.includes(it) }
+ .toSet()
+ val customHooks = customHookNames.toSet().flatMap { hookClassName ->
+ try {
+ loadHooks(Class.forName(hookClassName)).also {
+ println("INFO: Loaded ${it.size} hooks from $hookClassName")
+ }
+ } catch (_: ClassNotFoundException) {
+ println("WARN: Failed to load hooks from $hookClassName")
+ emptySet()
+ }
+ }
+ val relevantClassesLoadedAfterCustomHooks = instrumentation.allLoadedClasses
+ .map { it.name }
+ .filter { classNameGlobber.includes(it) || dependencyClassNameGlobber.includes(it) }
+ .toSet()
+ val nonHookClassesLoadedByHooks = relevantClassesLoadedAfterCustomHooks - relevantClassesLoadedBeforeCustomHooks
+ if (nonHookClassesLoadedByHooks.isNotEmpty()) {
+ println("WARN: Hooks were not applied to the following classes as they are dependencies of hooks:")
+ println("WARN: ${nonHookClassesLoadedByHooks.joinToString()}")
+ }
+
+ runtimeInstrumentor.registerCustomHooks(customHooks)
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel
new file mode 100644
index 00000000..2d5eec5c
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library")
+
+kt_jvm_library(
+ name = "agent_lib",
+ srcs = [
+ "Agent.kt",
+ "CoverageIdStrategy.kt",
+ "RuntimeInstrumentor.kt",
+ ],
+ visibility = ["//visibility:public"],
+ deps = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor",
+ "//agent/src/main/java/com/code_intelligence/jazzer/runtime",
+ ],
+)
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt b/agent/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt
new file mode 100644
index 00000000..fd2a1e7c
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt
@@ -0,0 +1,201 @@
+// 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.agent
+
+import java.nio.ByteBuffer
+import java.nio.channels.FileChannel
+import java.nio.channels.FileLock
+import java.nio.file.Path
+import java.nio.file.StandardOpenOption
+import java.util.UUID
+
+/**
+ * Indicates a fatal failure to generate synchronized coverage IDs.
+ */
+internal class CoverageIdException(cause: Throwable? = null) :
+ RuntimeException("Failed to synchronize coverage IDs", cause)
+
+interface CoverageIdStrategy {
+ /**
+ * Obtain the first coverage ID to be used for the class [className].
+ * The caller *must* also call [commitIdCount] once it has instrumented that class, even if instrumentation fails.
+ */
+ @Throws(CoverageIdException::class)
+ fun obtainFirstId(className: String): Int
+
+ /**
+ * Records the number of coverage IDs used to instrument the class specified in a previous call to [obtainFirstId].
+ * If instrumenting the class should fail, this function must still be called. In this case, [idCount] is set to 0.
+ */
+ @Throws(CoverageIdException::class)
+ fun commitIdCount(idCount: Int)
+}
+
+/**
+ * An unsynchronized strategy for coverage ID generation that simply increments a global counter.
+ */
+internal class TrivialCoverageIdStrategy : CoverageIdStrategy {
+ private var nextEdgeId = 0
+
+ override fun obtainFirstId(className: String) = nextEdgeId
+
+ override fun commitIdCount(idCount: Int) {
+ nextEdgeId += idCount
+ }
+}
+
+/**
+ * Reads the [FileChannel] to the end as a UTF-8 string.
+ */
+private 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].
+ */
+private fun FileChannel.append(string: String) {
+ position(size())
+ write(ByteBuffer.wrap(string.toByteArray()))
+}
+
+/**
+ * A strategy for coverage ID generation that synchronizes the IDs assigned to a class with other processes via the
+ * specified [idSyncFile].
+ * This class takes care of synchronizing the access to the file between multiple processes as long as the general
+ * contract of [CoverageIdStrategy] is followed.
+ *
+ * Rationale: Coverage (i.e., edge) IDs differ from other kinds of IDs, such as those generated for call sites or cmp
+ * instructions, in that they should be consecutive, collision-free, and lie in a known, small range. This precludes us
+ * from generating them simply as hashes of class names and explains why go through the arduous process of synchronizing
+ * them across multiple agents.
+ */
+internal class SynchronizedCoverageIdStrategy(private val idSyncFile: Path) : CoverageIdStrategy {
+ val uuid: UUID = UUID.randomUUID()
+ var idFileLock: FileLock? = null
+
+ var cachedFirstId: Int? = null
+ var cachedClassName: String? = null
+ var cachedIdCount: Int? = null
+
+ /**
+ * Obtains a coverage ID for [className] such that all cooperating agent processes will obtain the same ID.
+ * There are two cases to consider:
+ * - This agent process is the first to encounter [className], i.e., it does not find a record for that class in
+ * [idSyncFile]. In this case, a lock on the file is held until the class has been instrumented and a record with
+ * the required number of coverage IDs has been added.
+ * - Another agent process has already encountered [className], i.e., there is a record that class in [idSyncFile].
+ * In this case, the lock on the file is returned immediately and the extracted first coverage ID is returned to
+ * the caller. The caller is still expected to call [commitIdCount] so that desynchronization can be detected.
+ */
+ override fun obtainFirstId(className: String): Int {
+ try {
+ check(idFileLock == null) { "Already holding a lock on the ID file" }
+ val localIdFile = FileChannel.open(
+ idSyncFile,
+ StandardOpenOption.WRITE,
+ StandardOpenOption.READ
+ )
+ // Wait until we have obtained the lock on the sync file. We hold the lock from this point until we have
+ // finished reading and writing (if necessary) to the file.
+ val localIdFileLock = localIdFile.lock()
+ check(localIdFileLock.isValid && !localIdFileLock.isShared)
+ // Parse the sync file, which consists of lines of the form
+ // <class name>:<first ID>:<num IDs>
+ val idInfo = localIdFileLock.channel().readFully()
+ .lineSequence()
+ .filterNot { it.isBlank() }
+ .map { line ->
+ val parts = line.split(':')
+ check(parts.size == 4) {
+ "Expected ID file line to be of the form '<class name>:<first ID>:<num IDs>:<uuid>', got '$line'"
+ }
+ val lineClassName = parts[0]
+ val lineFirstId = parts[1].toInt()
+ check(lineFirstId >= 0) { "Negative first ID in line: $line" }
+ val lineIdCount = parts[2].toInt()
+ check(lineIdCount >= 0) { "Negative ID count in line: $line" }
+ Triple(lineClassName, lineFirstId, lineIdCount)
+ }.toList()
+ cachedClassName = className
+ val idInfoForClass = idInfo.filter { it.first == className }
+ return when (idInfoForClass.size) {
+ 0 -> {
+ // We are the first to encounter this class and thus need to hold the lock until the class has been
+ // instrumented and we know the required number of coverage IDs.
+ idFileLock = localIdFileLock
+ // Compute the next free ID as the maximum over the sums of first ID and ID count, starting at 0 if
+ // this is the first ID to be assigned. In fact, since this is the only way new lines are added to
+ // the file, the maximum is always attained by the last line.
+ val nextFreeId = idInfo.asSequence().map { it.second + it.third }.lastOrNull() ?: 0
+ cachedFirstId = nextFreeId
+ nextFreeId
+ }
+ 1 -> {
+ // This class has already been instrumented elsewhere, so we just return the first ID and ID count
+ // reported from there and release the lock right away. The caller is still expected to call
+ // commitIdCount.
+ localIdFile.close()
+ cachedIdCount = idInfoForClass.single().third
+ idInfoForClass.single().second
+ }
+ else -> {
+ localIdFile.close()
+ System.err.println(idInfo.joinToString("\n") { "${it.first}:${it.second}:${it.third}" })
+ throw IllegalStateException("Multiple entries for $className in ID file")
+ }
+ }
+ } catch (e: Exception) {
+ throw CoverageIdException(e)
+ }
+ }
+
+ override fun commitIdCount(idCount: Int) {
+ val localIdFileLock = idFileLock
+ try {
+ check(cachedClassName != null)
+ if (localIdFileLock == null) {
+ // We released the lock already in obtainFirstId since the class had already been instrumented
+ // elsewhere. As we know the expected number of IDs for the current class in this case, check for
+ // deviations.
+ check(cachedIdCount != null)
+ check(idCount == cachedIdCount) {
+ "$cachedClassName has $idCount edges, but $cachedIdCount edges reserved in ID file"
+ }
+ } else {
+ // We are the first to instrument this class and should record the number of IDs in the sync file.
+ check(cachedFirstId != null)
+ localIdFileLock.channel().append("$cachedClassName:$cachedFirstId:$idCount:$uuid\n")
+ localIdFileLock.channel().force(true)
+ }
+ idFileLock = null
+ cachedFirstId = null
+ cachedIdCount = null
+ cachedClassName = null
+ } catch (e: Exception) {
+ throw CoverageIdException(e)
+ } finally {
+ localIdFileLock?.channel()?.close()
+ }
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt
new file mode 100644
index 00000000..e2283aa2
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt
@@ -0,0 +1,182 @@
+// 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.agent
+
+import com.code_intelligence.jazzer.instrumentor.ClassInstrumentor
+import com.code_intelligence.jazzer.instrumentor.CoverageRecorder
+import com.code_intelligence.jazzer.instrumentor.Hook
+import com.code_intelligence.jazzer.instrumentor.InstrumentationType
+import com.code_intelligence.jazzer.instrumentor.loadHooks
+import com.code_intelligence.jazzer.runtime.NativeLibHooks
+import com.code_intelligence.jazzer.runtime.TraceCmpHooks
+import com.code_intelligence.jazzer.runtime.TraceDivHooks
+import com.code_intelligence.jazzer.runtime.TraceIndirHooks
+import com.code_intelligence.jazzer.utils.ClassNameGlobber
+import java.lang.instrument.ClassFileTransformer
+import java.lang.instrument.Instrumentation
+import java.nio.file.Path
+import java.security.ProtectionDomain
+import kotlin.math.roundToInt
+import kotlin.system.exitProcess
+import kotlin.time.measureTimedValue
+
+internal class RuntimeInstrumentor(
+ private val instrumentation: Instrumentation,
+ private val classesToInstrument: ClassNameGlobber,
+ private val dependencyClassesToInstrument: ClassNameGlobber,
+ private val instrumentationTypes: Set<InstrumentationType>,
+ idSyncFile: Path?,
+ private val dumpClassesDir: Path?,
+) : ClassFileTransformer {
+
+ private val coverageIdSynchronizer = if (idSyncFile != null)
+ SynchronizedCoverageIdStrategy(idSyncFile)
+ else
+ TrivialCoverageIdStrategy()
+
+ private val includedHooks = instrumentationTypes
+ .mapNotNull { type ->
+ when (type) {
+ InstrumentationType.CMP -> TraceCmpHooks::class.java
+ InstrumentationType.DIV -> TraceDivHooks::class.java
+ InstrumentationType.INDIR -> TraceIndirHooks::class.java
+ InstrumentationType.NATIVE -> NativeLibHooks::class.java
+ else -> null
+ }
+ }
+ .flatMap { loadHooks(it) }
+ private val customHooks = emptyList<Hook>().toMutableList()
+
+ fun registerCustomHooks(hooks: List<Hook>) {
+ customHooks.addAll(hooks)
+ }
+
+ @OptIn(kotlin.time.ExperimentalTime::class)
+ override fun transform(
+ loader: ClassLoader?,
+ internalClassName: String,
+ classBeingRedefined: Class<*>?,
+ protectionDomain: ProtectionDomain?,
+ classfileBuffer: ByteArray,
+ ): ByteArray? {
+ return try {
+ // Bail out early if we would instrument ourselves. This prevents ClassCircularityErrors as we might need to
+ // load additional Jazzer classes until we reach the full exclusion logic.
+ if (internalClassName.startsWith("com/code_intelligence/jazzer/"))
+ return null
+ transformInternal(internalClassName, classfileBuffer)
+ } catch (t: Throwable) {
+ // Throwables raised from transform are silently dropped, making it extremely hard to detect instrumentation
+ // failures. The docs advise to use a top-level try-catch.
+ // https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/ClassFileTransformer.html
+ t.printStackTrace()
+ throw t
+ }.also { instrumentedByteCode ->
+ // Only dump classes that were instrumented.
+ if (instrumentedByteCode != null && dumpClassesDir != null) {
+ val relativePath = "$internalClassName.class"
+ val absolutePath = dumpClassesDir.resolve(relativePath)
+ val dumpFile = absolutePath.toFile()
+ dumpFile.parentFile.mkdirs()
+ dumpFile.writeBytes(instrumentedByteCode)
+ }
+ }
+ }
+
+ override fun transform(
+ module: Module?,
+ loader: ClassLoader?,
+ internalClassName: String,
+ classBeingRedefined: Class<*>?,
+ protectionDomain: ProtectionDomain?,
+ classfileBuffer: ByteArray
+ ): ByteArray? {
+ if (module != null && !module.canRead(RuntimeInstrumentor::class.java.module)) {
+ // Make all other modules read our (unnamed) module, which allows them to access the classes needed by the
+ // instrumentations, e.g. CoverageMap. If a module can't be modified, it should not be instrumented as the
+ // injected bytecode might throw NoClassDefFoundError.
+ // https://mail.openjdk.java.net/pipermail/jigsaw-dev/2021-May/014663.html
+ if (!instrumentation.isModifiableModule(module)) {
+ val prettyClassName = internalClassName.replace('/', '.')
+ println("WARN: Failed to instrument $prettyClassName in unmodifiable module ${module.name}, skipping")
+ return null
+ }
+ instrumentation.redefineModule(
+ module,
+ /* extraReads */ setOf(RuntimeInstrumentor::class.java.module),
+ emptyMap(),
+ emptyMap(),
+ emptySet(),
+ emptyMap()
+ )
+ }
+ return transform(loader, internalClassName, classBeingRedefined, protectionDomain, classfileBuffer)
+ }
+
+ @OptIn(kotlin.time.ExperimentalTime::class)
+ fun transformInternal(internalClassName: String, classfileBuffer: ByteArray): ByteArray? {
+ val fullInstrumentation = when {
+ classesToInstrument.includes(internalClassName) -> true
+ dependencyClassesToInstrument.includes(internalClassName) -> false
+ else -> return null
+ }
+ val prettyClassName = internalClassName.replace('/', '.')
+ val (instrumentedBytecode, duration) = measureTimedValue {
+ try {
+ instrument(internalClassName, classfileBuffer, fullInstrumentation)
+ } catch (e: CoverageIdException) {
+ System.err.println("ERROR: Coverage IDs are out of sync")
+ e.printStackTrace()
+ exitProcess(1)
+ } catch (e: Exception) {
+ println("WARN: Failed to instrument $prettyClassName, skipping")
+ e.printStackTrace()
+ return null
+ }
+ }
+ val durationInMs = duration.inWholeMilliseconds
+ val sizeIncrease = ((100.0 * (instrumentedBytecode.size - classfileBuffer.size)) / classfileBuffer.size).roundToInt()
+ if (fullInstrumentation) {
+ println("INFO: Instrumented $prettyClassName (took $durationInMs ms, size +$sizeIncrease%)")
+ } else {
+ println("INFO: Instrumented $prettyClassName with custom hooks only (took $durationInMs ms, size +$sizeIncrease%)")
+ }
+ return instrumentedBytecode
+ }
+
+ private fun instrument(internalClassName: String, bytecode: ByteArray, fullInstrumentation: Boolean): ByteArray {
+ return ClassInstrumentor(bytecode).run {
+ if (fullInstrumentation) {
+ // Hook instrumentation must be performed after data flow tracing as the injected
+ // bytecode would trigger the GEP callbacks for byte[]. Coverage instrumentation
+ // must be performed after hook instrumentation as the injected bytecode would
+ // trigger the GEP callbacks for ByteBuffer.
+ traceDataFlow(instrumentationTypes)
+ hooks(includedHooks + customHooks)
+ val firstId = coverageIdSynchronizer.obtainFirstId(internalClassName)
+ var actualNumEdgeIds = 0
+ try {
+ actualNumEdgeIds = coverage(firstId)
+ } finally {
+ coverageIdSynchronizer.commitIdCount(actualNumEdgeIds)
+ }
+ CoverageRecorder.recordInstrumentedClass(internalClassName, bytecode, firstId, firstId + actualNumEdgeIds)
+ } else {
+ hooks(customHooks)
+ }
+ instrumentedBytecode
+ }
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/AutofuzzConstructionException.java b/agent/src/main/java/com/code_intelligence/jazzer/api/AutofuzzConstructionException.java
new file mode 100644
index 00000000..93340ee8
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/AutofuzzConstructionException.java
@@ -0,0 +1,32 @@
+// 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.api;
+
+// An exception wrapping a Throwable thrown during the construction of parameters for, but not the
+// actual invocation of an autofuzzed method.
+/**
+ * Only used internally.
+ */
+public class AutofuzzConstructionException extends RuntimeException {
+ public AutofuzzConstructionException() {
+ super();
+ }
+ public AutofuzzConstructionException(String message) {
+ super(message);
+ }
+ public AutofuzzConstructionException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/AutofuzzInvocationException.java b/agent/src/main/java/com/code_intelligence/jazzer/api/AutofuzzInvocationException.java
new file mode 100644
index 00000000..7e6203ce
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/AutofuzzInvocationException.java
@@ -0,0 +1,26 @@
+// 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.api;
+
+// An exception wrapping a {@link Throwable} thrown during the actual invocation of, but not the
+// construction of parameters for an autofuzzed method.
+/**
+ * Only used internally.
+ */
+public class AutofuzzInvocationException extends RuntimeException {
+ public AutofuzzInvocationException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel
new file mode 100644
index 00000000..e573e757
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel
@@ -0,0 +1,28 @@
+java_library(
+ name = "api",
+ srcs = [
+ "AutofuzzConstructionException.java",
+ "AutofuzzInvocationException.java",
+ "CannedFuzzedDataProvider.java",
+ "Consumer1.java",
+ "Consumer2.java",
+ "Consumer3.java",
+ "Consumer4.java",
+ "Consumer5.java",
+ "Function1.java",
+ "Function2.java",
+ "Function3.java",
+ "Function4.java",
+ "Function5.java",
+ "FuzzedDataProvider.java",
+ "FuzzerSecurityIssueCritical.java",
+ "FuzzerSecurityIssueHigh.java",
+ "FuzzerSecurityIssueLow.java",
+ "FuzzerSecurityIssueMedium.java",
+ "HookType.java",
+ "Jazzer.java",
+ "MethodHook.java",
+ "MethodHooks.java",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/CannedFuzzedDataProvider.java b/agent/src/main/java/com/code_intelligence/jazzer/api/CannedFuzzedDataProvider.java
new file mode 100644
index 00000000..7209a497
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/CannedFuzzedDataProvider.java
@@ -0,0 +1,211 @@
+// 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.api;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Replays recorded FuzzedDataProvider invocations that were executed while fuzzing.
+ * Note: This class is only meant to be used by Jazzer's generated reproducers.
+ */
+final public class CannedFuzzedDataProvider implements FuzzedDataProvider {
+ private final Iterator<Object> nextReply;
+
+ public CannedFuzzedDataProvider(String can) {
+ byte[] rawIn = Base64.getDecoder().decode(can);
+ ArrayList<Object> recordedReplies;
+ try (ByteArrayInputStream byteStream = new ByteArrayInputStream(rawIn)) {
+ try (ObjectInputStream objectStream = new ObjectInputStream(byteStream)) {
+ recordedReplies = (ArrayList<Object>) objectStream.readObject();
+ }
+ } catch (IOException | ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ nextReply = recordedReplies.iterator();
+ }
+
+ public static CannedFuzzedDataProvider create(List<Object> objects) {
+ try {
+ try (ByteArrayOutputStream bout = new ByteArrayOutputStream()) {
+ try (ObjectOutputStream out = new ObjectOutputStream(bout)) {
+ out.writeObject(new ArrayList<>(objects));
+ String base64 = Base64.getEncoder().encodeToString(bout.toByteArray());
+ return new CannedFuzzedDataProvider(base64);
+ }
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Override
+ public boolean consumeBoolean() {
+ return (boolean) nextReply.next();
+ }
+
+ @Override
+ public boolean[] consumeBooleans(int maxLength) {
+ return (boolean[]) nextReply.next();
+ }
+
+ @Override
+ public byte consumeByte() {
+ return (byte) nextReply.next();
+ }
+
+ @Override
+ public byte consumeByte(byte min, byte max) {
+ return (byte) nextReply.next();
+ }
+
+ @Override
+ public short consumeShort() {
+ return (short) nextReply.next();
+ }
+
+ @Override
+ public short consumeShort(short min, short max) {
+ return (short) nextReply.next();
+ }
+
+ @Override
+ public short[] consumeShorts(int maxLength) {
+ return (short[]) nextReply.next();
+ }
+
+ @Override
+ public int consumeInt() {
+ return (int) nextReply.next();
+ }
+
+ @Override
+ public int consumeInt(int min, int max) {
+ return (int) nextReply.next();
+ }
+
+ @Override
+ public int[] consumeInts(int maxLength) {
+ return (int[]) nextReply.next();
+ }
+
+ @Override
+ public long consumeLong() {
+ return (long) nextReply.next();
+ }
+
+ @Override
+ public long consumeLong(long min, long max) {
+ return (long) nextReply.next();
+ }
+
+ @Override
+ public long[] consumeLongs(int maxLength) {
+ return (long[]) nextReply.next();
+ }
+
+ @Override
+ public float consumeFloat() {
+ return (float) nextReply.next();
+ }
+
+ @Override
+ public float consumeRegularFloat() {
+ return (float) nextReply.next();
+ }
+
+ @Override
+ public float consumeRegularFloat(float min, float max) {
+ return (float) nextReply.next();
+ }
+
+ @Override
+ public float consumeProbabilityFloat() {
+ return (float) nextReply.next();
+ }
+
+ @Override
+ public double consumeDouble() {
+ return (double) nextReply.next();
+ }
+
+ @Override
+ public double consumeRegularDouble(double min, double max) {
+ return (double) nextReply.next();
+ }
+
+ @Override
+ public double consumeRegularDouble() {
+ return (double) nextReply.next();
+ }
+
+ @Override
+ public double consumeProbabilityDouble() {
+ return (double) nextReply.next();
+ }
+
+ @Override
+ public char consumeChar() {
+ return (char) nextReply.next();
+ }
+
+ @Override
+ public char consumeChar(char min, char max) {
+ return (char) nextReply.next();
+ }
+
+ @Override
+ public char consumeCharNoSurrogates() {
+ return (char) nextReply.next();
+ }
+
+ @Override
+ public String consumeAsciiString(int maxLength) {
+ return (String) nextReply.next();
+ }
+
+ @Override
+ public String consumeString(int maxLength) {
+ return (String) nextReply.next();
+ }
+
+ @Override
+ public String consumeRemainingAsAsciiString() {
+ return (String) nextReply.next();
+ }
+
+ @Override
+ public String consumeRemainingAsString() {
+ return (String) nextReply.next();
+ }
+
+ @Override
+ public byte[] consumeBytes(int maxLength) {
+ return (byte[]) nextReply.next();
+ }
+
+ @Override
+ public byte[] consumeRemainingAsBytes() {
+ return (byte[]) nextReply.next();
+ }
+
+ @Override
+ public int remainingBytes() {
+ return (int) nextReply.next();
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer1.java b/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer1.java
new file mode 100644
index 00000000..472c2efd
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer1.java
@@ -0,0 +1,22 @@
+// 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.api;
+
+import java.util.function.Consumer;
+
+@FunctionalInterface
+public interface Consumer1<T1> extends Consumer<T1> {
+ @Override void accept(T1 t1);
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer2.java b/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer2.java
new file mode 100644
index 00000000..d951ade7
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer2.java
@@ -0,0 +1,22 @@
+// 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.api;
+
+import java.util.function.BiConsumer;
+
+@FunctionalInterface
+public interface Consumer2<T1, T2> extends BiConsumer<T1, T2> {
+ @Override void accept(T1 t1, T2 t2);
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer3.java b/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer3.java
new file mode 100644
index 00000000..c508fe53
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer3.java
@@ -0,0 +1,20 @@
+// 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.api;
+
+@FunctionalInterface
+public interface Consumer3<T1, T2, T3> {
+ void accept(T1 t1, T2 t2, T3 t3);
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer4.java b/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer4.java
new file mode 100644
index 00000000..6ee70141
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer4.java
@@ -0,0 +1,20 @@
+// 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.api;
+
+@FunctionalInterface
+public interface Consumer4<T1, T2, T3, T4> {
+ void accept(T1 t1, T2 t2, T3 t3, T4 t4);
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer5.java b/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer5.java
new file mode 100644
index 00000000..523df53c
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/Consumer5.java
@@ -0,0 +1,20 @@
+// 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.api;
+
+@FunctionalInterface
+public interface Consumer5<T1, T2, T3, T4, T5> {
+ void accept(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5);
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/Function1.java b/agent/src/main/java/com/code_intelligence/jazzer/api/Function1.java
new file mode 100644
index 00000000..43d68cc7
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/Function1.java
@@ -0,0 +1,22 @@
+// 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.api;
+
+import java.util.function.Function;
+
+@FunctionalInterface
+public interface Function1<T1, R> extends Function<T1, R> {
+ @Override R apply(T1 t1);
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/Function2.java b/agent/src/main/java/com/code_intelligence/jazzer/api/Function2.java
new file mode 100644
index 00000000..6e733b1c
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/Function2.java
@@ -0,0 +1,22 @@
+// 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.api;
+
+import java.util.function.BiFunction;
+
+@FunctionalInterface
+public interface Function2<T1, T2, R> extends BiFunction<T1, T2, R> {
+ @Override R apply(T1 t1, T2 t2);
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/Function3.java b/agent/src/main/java/com/code_intelligence/jazzer/api/Function3.java
new file mode 100644
index 00000000..07d593f9
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/Function3.java
@@ -0,0 +1,20 @@
+// 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.api;
+
+@FunctionalInterface
+public interface Function3<T1, T2, T3, R> {
+ R apply(T1 t1, T2 t2, T3 t3);
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/Function4.java b/agent/src/main/java/com/code_intelligence/jazzer/api/Function4.java
new file mode 100644
index 00000000..0e6ec75e
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/Function4.java
@@ -0,0 +1,20 @@
+// 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.api;
+
+@FunctionalInterface
+public interface Function4<T1, T2, T3, T4, R> {
+ R apply(T1 t1, T2 t2, T3 t3, T4 t4);
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/Function5.java b/agent/src/main/java/com/code_intelligence/jazzer/api/Function5.java
new file mode 100644
index 00000000..cd833f78
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/Function5.java
@@ -0,0 +1,20 @@
+// 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.api;
+
+@FunctionalInterface
+public interface Function5<T1, T2, T3, T4, T5, R> {
+ R apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5);
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzedDataProvider.java b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzedDataProvider.java
new file mode 100644
index 00000000..b1f38b50
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzedDataProvider.java
@@ -0,0 +1,444 @@
+// 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.api;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * A convenience wrapper turning the raw fuzzer input bytes into Java primitive types.
+ *
+ * <p>The methods defined by this interface behave similarly to {@link Random#nextInt()}, with all
+ * returned values depending deterministically on the fuzzer input for the current run.
+ */
+public interface FuzzedDataProvider {
+ /**
+ * Consumes a {@code boolean} from the fuzzer input.
+ *
+ * @return a {@code boolean}
+ */
+ boolean consumeBoolean();
+
+ /**
+ * Consumes a {@code boolean} array from the fuzzer input.
+ * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input
+ * is not sufficiently long.
+ *
+ * @param maxLength the maximum length of the array
+ * @return a {@code boolean} array of length at most {@code length}
+ */
+ boolean[] consumeBooleans(int maxLength);
+
+ /**
+ * Consumes a {@code byte} from the fuzzer input.
+ *
+ * @return a {@code byte}
+ */
+ byte consumeByte();
+
+ /**
+ * Consumes a {@code byte} between {@code min} and {@code max} from the fuzzer input.
+ *
+ * @param min the inclusive lower bound on the returned value
+ * @param max the inclusive upper bound on the returned value
+ * @return a {@code byte} in the range {@code [min, max]}
+ */
+ byte consumeByte(byte min, byte max);
+
+ /**
+ * Consumes a {@code byte} array from the fuzzer input.
+ * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input
+ * is not sufficiently long.
+ *
+ * @param maxLength the maximum length of the array
+ * @return a {@code byte} array of length at most {@code length}
+ */
+ byte[] consumeBytes(int maxLength);
+
+ /**
+ * Consumes the remaining fuzzer input as a {@code byte} array.
+ * <p><b>Note:</b> After calling this method, further calls to methods of this interface will
+ * return fixed values only.
+ *
+ * @return a {@code byte} array
+ */
+ byte[] consumeRemainingAsBytes();
+
+ /**
+ * Consumes a {@code short} from the fuzzer input.
+ *
+ * @return a {@code short}
+ */
+ short consumeShort();
+
+ /**
+ * Consumes a {@code short} between {@code min} and {@code max} from the fuzzer input.
+ *
+ * @param min the inclusive lower bound on the returned value
+ * @param max the inclusive upper bound on the returned value
+ * @return a {@code short} in the range {@code [min, max]}
+ */
+ short consumeShort(short min, short max);
+
+ /**
+ * Consumes a {@code short} array from the fuzzer input.
+ * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input
+ * is not sufficiently long.
+ *
+ * @param maxLength the maximum length of the array
+ * @return a {@code short} array of length at most {@code length}
+ */
+ short[] consumeShorts(int maxLength);
+
+ /**
+ * Consumes an {@code int} from the fuzzer input.
+ *
+ * @return an {@code int}
+ */
+ int consumeInt();
+
+ /**
+ * Consumes an {@code int} between {@code min} and {@code max} from the fuzzer input.
+ *
+ * @param min the inclusive lower bound on the returned value
+ * @param max the inclusive upper bound on the returned value
+ * @return an {@code int} in the range {@code [min, max]}
+ */
+ int consumeInt(int min, int max);
+
+ /**
+ * Consumes an {@code int} array from the fuzzer input.
+ * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input
+ * is not sufficiently long.
+ *
+ * @param maxLength the maximum length of the array
+ * @return an {@code int} array of length at most {@code length}
+ */
+ int[] consumeInts(int maxLength);
+
+ /**
+ * Consumes a {@code long} from the fuzzer input.
+ *
+ * @return a {@code long}
+ */
+ long consumeLong();
+
+ /**
+ * Consumes a {@code long} between {@code min} and {@code max} from the fuzzer input.
+ *
+ * @param min the inclusive lower bound on the returned value
+ * @param max the inclusive upper bound on the returned value
+ * @return a {@code long} in the range @{code [min, max]}
+ */
+ long consumeLong(long min, long max);
+
+ /**
+ * Consumes a {@code long} array from the fuzzer input.
+ * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input
+ * is not sufficiently long.
+ *
+ * @param maxLength the maximum length of the array
+ * @return a {@code long} array of length at most {@code length}
+ */
+ long[] consumeLongs(int maxLength);
+
+ /**
+ * Consumes a {@code float} from the fuzzer input.
+ *
+ * @return a {@code float} that may have a special value (e.g. a NaN or infinity)
+ */
+ float consumeFloat();
+
+ /**
+ * Consumes a regular {@code float} from the fuzzer input.
+ *
+ * @return a {@code float} that is not a special value (e.g. not a NaN or infinity)
+ */
+ float consumeRegularFloat();
+
+ /**
+ * Consumes a regular {@code float} between {@code min} and {@code max} from the fuzzer input.
+ *
+ * @return a {@code float} in the range {@code [min, max]}
+ */
+ float consumeRegularFloat(float min, float max);
+
+ /**
+ * Consumes a {@code float} between 0.0 and 1.0 (inclusive) from the fuzzer input.
+ *
+ * @return a {@code float} in the range {@code [0.0, 1.0]}
+ */
+ float consumeProbabilityFloat();
+
+ /**
+ * Consumes a {@code double} from the fuzzer input.
+ *
+ * @return a {@code double} that may have a special value (e.g. a NaN or infinity)
+ */
+ double consumeDouble();
+
+ /**
+ * Consumes a regular {@code double} from the fuzzer input.
+ *
+ * @return a {@code double} that is not a special value (e.g. not a NaN or infinity)
+ */
+ double consumeRegularDouble();
+
+ /**
+ * Consumes a regular {@code double} between {@code min} and {@code max} from the fuzzer input.
+ *
+ * @return a {@code double} in the range {@code [min, max]}
+ */
+ double consumeRegularDouble(double min, double max);
+
+ /**
+ * Consumes a {@code double} between 0.0 and 1.0 (inclusive) from the fuzzer input.
+ *
+ * @return a {@code double} in the range {@code [0.0, 1.0]}
+ */
+ double consumeProbabilityDouble();
+
+ /**
+ * Consumes a {@code char} from the fuzzer input.
+ */
+ char consumeChar();
+
+ /**
+ * Consumes a {@code char} between {@code min} and {@code max} from the fuzzer input.
+ *
+ * @param min the inclusive lower bound on the returned value
+ * @param max the inclusive upper bound on the returned value
+ * @return a {@code char} in the range {@code [min, max]}
+ */
+ char consumeChar(char min, char max);
+
+ /**
+ * Consumes a {@code char} from the fuzzer input that is never a UTF-16 surrogate character.
+ */
+ char consumeCharNoSurrogates();
+
+ /**
+ * Consumes a {@link String} from the fuzzer input.
+ * <p>The returned string may be of any length between 0 and {@code maxLength}, even if there is
+ * more fuzzer input available.
+ *
+ * @param maxLength the maximum length of the string
+ * @return a {@link String} of length between 0 and {@code maxLength} (inclusive)
+ */
+ String consumeString(int maxLength);
+
+ /**
+ * Consumes the remaining fuzzer input as a {@link String}.
+ * <p><b>Note:</b> After calling this method, further calls to methods of this interface will
+ * return fixed values only.
+ *
+ * @return a {@link String}
+ */
+ String consumeRemainingAsString();
+
+ /**
+ * Consumes an ASCII-only {@link String} from the fuzzer input.
+ * <p>The returned string may be of any length between 0 and {@code maxLength}, even if there is
+ * more fuzzer input available.
+ *
+ * @param maxLength the maximum length of the string
+ * @return a {@link String} of length between 0 and {@code maxLength} (inclusive) that contains
+ * only ASCII characters
+ */
+ String consumeAsciiString(int maxLength);
+
+ /**
+ * Consumes the remaining fuzzer input as an ASCII-only {@link String}.
+ * <p><b>Note:</b> After calling this method, further calls to methods of this interface will
+ * return fixed values only.
+ *
+ * @return a {@link String} that contains only ASCII characters
+ */
+ String consumeRemainingAsAsciiString();
+
+ /**
+ * Returns the number of unconsumed bytes in the fuzzer input.
+ *
+ * @return the number of unconsumed bytes in the fuzzer input
+ */
+ int remainingBytes();
+
+ /**
+ * Picks an element from {@code collection} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param collection the {@link Collection} to pick an element from.
+ * @param <T> the type of the collection element
+ * @return an element from {@code collection} chosen based on the fuzzer input
+ */
+ @SuppressWarnings("unchecked")
+ default<T> T pickValue(Collection<T> collection) {
+ int size = collection.size();
+ if (size == 0) {
+ throw new IllegalArgumentException("collection is empty");
+ }
+ if (collection instanceof List<?>) {
+ return ((List<T>) collection).get(consumeInt(0, size - 1));
+ } else {
+ return (T) pickValue(collection.toArray());
+ }
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @param <T> the type of the array element
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default<T> T pickValue(T[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default boolean pickValue(boolean[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default byte pickValue(byte[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default short pickValue(short[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default int pickValue(int[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default long pickValue(long[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default double pickValue(double[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default float pickValue(float[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks an element from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @return an element from {@code array} chosen based on the fuzzer input
+ */
+ default char pickValue(char[] array) {
+ return array[consumeInt(0, array.length - 1)];
+ }
+
+ /**
+ * Picks {@code numOfElements} elements from {@code collection} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param collection the {@link Collection} to pick an element from.
+ * @param numOfElements the number of elements to pick.
+ * @param <T> the type of the collection element
+ * @return an array of size {@code numOfElements} from {@code collection} chosen based on the
+ * fuzzer input
+ */
+ default<T> List<T> pickValues(Collection<T> collection, int numOfElements) {
+ int size = collection.size();
+ if (size == 0) {
+ throw new IllegalArgumentException("collection is empty");
+ }
+ if (numOfElements > collection.size()) {
+ throw new IllegalArgumentException("numOfElements exceeds collection.size()");
+ }
+
+ List<T> remainingElements = new ArrayList<>(collection);
+ List<T> pickedElements = new ArrayList<>();
+ for (int i = 0; i < numOfElements; i++) {
+ T element = pickValue(remainingElements);
+ pickedElements.add(element);
+ remainingElements.remove(element);
+ }
+ return pickedElements;
+ }
+
+ /**
+ * Picks {@code numOfElements} elements from {@code array} based on the fuzzer input.
+ * <p><b>Note:</b> The distribution of picks is not perfectly uniform.
+ *
+ * @param array the array to pick an element from.
+ * @param numOfElements the number of elements to pick.
+ * @param <T> the type of the array element
+ * @return an array of size {@code numOfElements} from {@code array} chosen based on the fuzzer
+ * input
+ */
+ default<T> List<T> pickValues(T[] array, int numOfElements) {
+ return pickValues(Arrays.asList(array), numOfElements);
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java
new file mode 100644
index 00000000..4402a7f3
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java
@@ -0,0 +1,39 @@
+// 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.api;
+
+/**
+ * Thrown to indicate that a fuzz target has detected a critical severity security issue rather than
+ * a normal bug.
+ *
+ * There is only a semantical but no functional difference between throwing exceptions of this type
+ * or any other. However, automated fuzzing platforms can use the extra information to handle the
+ * detected issues appropriately.
+ */
+public class FuzzerSecurityIssueCritical extends RuntimeException {
+ public FuzzerSecurityIssueCritical() {}
+
+ public FuzzerSecurityIssueCritical(String message) {
+ super(message);
+ }
+
+ public FuzzerSecurityIssueCritical(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public FuzzerSecurityIssueCritical(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java
new file mode 100644
index 00000000..4d323e56
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java
@@ -0,0 +1,39 @@
+// 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.api;
+
+/**
+ * Thrown to indicate that a fuzz target has detected a high severity security issue rather than a
+ * normal bug.
+ *
+ * There is only a semantical but no functional difference between throwing exceptions of this type
+ * or any other. However, automated fuzzing platforms can use the extra information to handle the
+ * detected issues appropriately.
+ */
+public class FuzzerSecurityIssueHigh extends RuntimeException {
+ public FuzzerSecurityIssueHigh() {}
+
+ public FuzzerSecurityIssueHigh(String message) {
+ super(message);
+ }
+
+ public FuzzerSecurityIssueHigh(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public FuzzerSecurityIssueHigh(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueLow.java b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueLow.java
new file mode 100644
index 00000000..364b3afb
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueLow.java
@@ -0,0 +1,39 @@
+// 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.api;
+
+/**
+ * Thrown to indicate that a fuzz target has detected a low severity security issue rather than a
+ * normal bug.
+ *
+ * There is only a semantical but no functional difference between throwing exceptions of this type
+ * or any other. However, automated fuzzing platforms can use the extra information to handle the
+ * detected issues appropriately.
+ */
+public class FuzzerSecurityIssueLow extends RuntimeException {
+ public FuzzerSecurityIssueLow() {}
+
+ public FuzzerSecurityIssueLow(String message) {
+ super(message);
+ }
+
+ public FuzzerSecurityIssueLow(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public FuzzerSecurityIssueLow(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java
new file mode 100644
index 00000000..f0de4ce7
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java
@@ -0,0 +1,39 @@
+// 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.api;
+
+/**
+ * Thrown to indicate that a fuzz target has detected a medium severity security issue rather than a
+ * normal bug.
+ *
+ * There is only a semantical but no functional difference between throwing exceptions of this type
+ * or any other. However, automated fuzzing platforms can use the extra information to handle the
+ * detected issues appropriately.
+ */
+public class FuzzerSecurityIssueMedium extends RuntimeException {
+ public FuzzerSecurityIssueMedium() {}
+
+ public FuzzerSecurityIssueMedium(String message) {
+ super(message);
+ }
+
+ public FuzzerSecurityIssueMedium(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public FuzzerSecurityIssueMedium(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/HookType.java b/agent/src/main/java/com/code_intelligence/jazzer/api/HookType.java
new file mode 100644
index 00000000..1c564a78
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/HookType.java
@@ -0,0 +1,24 @@
+// 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.api;
+
+/**
+ * The type of a {@link MethodHook}.
+ */
+public enum HookType {
+ BEFORE,
+ REPLACE,
+ AFTER,
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java b/agent/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java
new file mode 100644
index 00000000..e45f7600
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java
@@ -0,0 +1,499 @@
+// 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.api;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Helper class with static methods that interact with Jazzer at runtime.
+ */
+final public class Jazzer {
+ private static Class<?> jazzerInternal = null;
+
+ private static MethodHandle traceStrcmp = null;
+ private static MethodHandle traceStrstr = null;
+ private static MethodHandle traceMemcmp = null;
+
+ private static MethodHandle consume = null;
+ private static MethodHandle autofuzzFunction1 = null;
+ private static MethodHandle autofuzzFunction2 = null;
+ private static MethodHandle autofuzzFunction3 = null;
+ private static MethodHandle autofuzzFunction4 = null;
+ private static MethodHandle autofuzzFunction5 = null;
+ private static MethodHandle autofuzzConsumer1 = null;
+ private static MethodHandle autofuzzConsumer2 = null;
+ private static MethodHandle autofuzzConsumer3 = null;
+ private static MethodHandle autofuzzConsumer4 = null;
+ private static MethodHandle autofuzzConsumer5 = null;
+
+ static {
+ try {
+ jazzerInternal = Class.forName("com.code_intelligence.jazzer.runtime.JazzerInternal");
+ Class<?> traceDataFlowNativeCallbacks =
+ Class.forName("com.code_intelligence.jazzer.runtime.TraceDataFlowNativeCallbacks");
+
+ // Use method handles for hints as the calls are potentially performance critical.
+ MethodType traceStrcmpType =
+ MethodType.methodType(void.class, String.class, String.class, int.class, int.class);
+ traceStrcmp = MethodHandles.publicLookup().findStatic(
+ traceDataFlowNativeCallbacks, "traceStrcmp", traceStrcmpType);
+ MethodType traceStrstrType =
+ MethodType.methodType(void.class, String.class, String.class, int.class);
+ traceStrstr = MethodHandles.publicLookup().findStatic(
+ traceDataFlowNativeCallbacks, "traceStrstr", traceStrstrType);
+ MethodType traceMemcmpType =
+ MethodType.methodType(void.class, byte[].class, byte[].class, int.class, int.class);
+ traceMemcmp = MethodHandles.publicLookup().findStatic(
+ traceDataFlowNativeCallbacks, "traceMemcmp", traceMemcmpType);
+
+ Class<?> metaClass = Class.forName("com.code_intelligence.jazzer.autofuzz.Meta");
+ MethodType consumeType =
+ MethodType.methodType(Object.class, FuzzedDataProvider.class, Class.class);
+ consume = MethodHandles.publicLookup().findStatic(metaClass, "consume", consumeType);
+
+ autofuzzFunction1 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(Object.class, FuzzedDataProvider.class, Function1.class));
+ autofuzzFunction2 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(Object.class, FuzzedDataProvider.class, Function2.class));
+ autofuzzFunction3 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(Object.class, FuzzedDataProvider.class, Function3.class));
+ autofuzzFunction4 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(Object.class, FuzzedDataProvider.class, Function4.class));
+ autofuzzFunction5 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(Object.class, FuzzedDataProvider.class, Function5.class));
+ autofuzzConsumer1 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer1.class));
+ autofuzzConsumer2 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer2.class));
+ autofuzzConsumer3 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer3.class));
+ autofuzzConsumer4 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer4.class));
+ autofuzzConsumer5 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz",
+ MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer5.class));
+ } catch (ClassNotFoundException ignore) {
+ // Not running in the context of the agent. This is fine as long as no methods are called on
+ // this class.
+ } catch (NoSuchMethodException | IllegalAccessException e) {
+ // This should never happen as the Jazzer API is loaded from the agent and thus should always
+ // match the version of the runtime classes.
+ System.err.println("ERROR: Incompatible version of the Jazzer API detected, please update.");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private Jazzer() {}
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ *
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Function1} with (partially) specified
+ * type variables, e.g. {@code (Function1<String, ?>) String::new}.
+ * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke
+ * the function.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T1, R> R autofuzz(FuzzedDataProvider data, Function1<T1, R> func) {
+ try {
+ return (R) autofuzzFunction1.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ // Not reached.
+ return null;
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ *
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Function2} with (partially) specified
+ * type variables.
+ * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke
+ * the function.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, R> R autofuzz(FuzzedDataProvider data, Function2<T1, T2, R> func) {
+ try {
+ return (R) autofuzzFunction2.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ // Not reached.
+ return null;
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ *
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Function3} with (partially) specified
+ * type variables.
+ * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke
+ * the function.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, R> R autofuzz(FuzzedDataProvider data, Function3<T1, T2, T3, R> func) {
+ try {
+ return (R) autofuzzFunction3.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ // Not reached.
+ return null;
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ *
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Function4} with (partially) specified
+ * type variables.
+ * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke
+ * the function.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, T4, R> R autofuzz(
+ FuzzedDataProvider data, Function4<T1, T2, T3, T4, R> func) {
+ try {
+ return (R) autofuzzFunction4.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ // Not reached.
+ return null;
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ *
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Function5} with (partially) specified
+ * type variables.
+ * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke
+ * the function.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, T4, T5, R> R autofuzz(
+ FuzzedDataProvider data, Function5<T1, T2, T3, T4, T5, R> func) {
+ try {
+ return (R) autofuzzFunction5.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ // Not reached.
+ return null;
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ *
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Consumer1} with explicitly specified
+ * type variable.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ public static <T1> void autofuzz(FuzzedDataProvider data, Consumer1<T1> func) {
+ try {
+ autofuzzConsumer1.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ *
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Consumer2} with (partially) specified
+ * type variables.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ public static <T1, T2> void autofuzz(FuzzedDataProvider data, Consumer2<T1, T2> func) {
+ try {
+ autofuzzConsumer2.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ *
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Consumer3} with (partially) specified
+ * type variables.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ public static <T1, T2, T3> void autofuzz(FuzzedDataProvider data, Consumer3<T1, T2, T3> func) {
+ try {
+ autofuzzConsumer3.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ *
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Consumer4} with (partially) specified
+ * type variables.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ public static <T1, T2, T3, T4> void autofuzz(
+ FuzzedDataProvider data, Consumer4<T1, T2, T3, T4> func) {
+ try {
+ autofuzzConsumer4.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ }
+
+ /**
+ * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input
+ * using only public methods available on the classpath.
+ *
+ * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in
+ * meaningful ways for a number of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param func a method reference for the function to autofuzz. If there are multiple overloads,
+ * resolve ambiguities by explicitly casting to {@link Consumer5} with (partially) specified
+ * type variables.
+ * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link
+ * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call.
+ * The {@link Throwable} is thrown unchecked.
+ */
+ public static <T1, T2, T3, T4, T5> void autofuzz(
+ FuzzedDataProvider data, Consumer5<T1, T2, T3, T4, T5> func) {
+ try {
+ autofuzzConsumer5.invoke(data, func);
+ } catch (AutofuzzInvocationException e) {
+ rethrowUnchecked(e.getCause());
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ }
+ }
+
+ /**
+ * Attempts to construct an instance of {@code type} from the fuzzer input using only public
+ * methods available on the classpath.
+ *
+ * <b>Note:</b> This function is inherently heuristic and may fail to return meaningful values for
+ * a variety of reasons.
+ *
+ * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}.
+ * @param type the {@link Class} to construct an instance of.
+ * @return an instance of {@code type} constructed from the fuzzer input, or {@code null} if
+ * autofuzz failed to create an instance.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T consume(FuzzedDataProvider data, Class<T> type) {
+ try {
+ return (T) consume.invokeExact(data, type);
+ } catch (AutofuzzConstructionException ignored) {
+ return null;
+ } catch (Throwable t) {
+ rethrowUnchecked(t);
+ // Not reached.
+ return null;
+ }
+ }
+
+ /**
+ * Instructs the fuzzer to guide its mutations towards making {@code current} equal to {@code
+ * target}.
+ *
+ * If the relation between the raw fuzzer input and the value of {@code current} is relatively
+ * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to
+ * achieve equality.
+ *
+ * @param current a non-constant string observed during fuzz target execution
+ * @param target a string that {@code current} should become equal to, but currently isn't
+ * @param id a (probabilistically) unique identifier for this particular compare hint
+ */
+ public static void guideTowardsEquality(String current, String target, int id) {
+ try {
+ traceStrcmp.invokeExact(current, target, 1, id);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Instructs the fuzzer to guide its mutations towards making {@code current} equal to {@code
+ * target}.
+ *
+ * If the relation between the raw fuzzer input and the value of {@code current} is relatively
+ * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to
+ * achieve equality.
+ *
+ * @param current a non-constant byte array observed during fuzz target execution
+ * @param target a byte array that {@code current} should become equal to, but currently isn't
+ * @param id a (probabilistically) unique identifier for this particular compare hint
+ */
+ public static void guideTowardsEquality(byte[] current, byte[] target, int id) {
+ try {
+ traceMemcmp.invokeExact(current, target, 1, id);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Instructs the fuzzer to guide its mutations towards making {@code haystack} contain {@code
+ * needle} as a substring.
+ *
+ * If the relation between the raw fuzzer input and the value of {@code haystack} is relatively
+ * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to
+ * satisfy the substring check.
+ *
+ * @param haystack a non-constant string observed during fuzz target execution
+ * @param needle a string that should be contained in {@code haystack} as a substring, but
+ * currently isn't
+ * @param id a (probabilistically) unique identifier for this particular compare hint
+ */
+ public static void guideTowardsContainment(String haystack, String needle, int id) {
+ try {
+ traceStrstr.invokeExact(haystack, needle, id);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Make Jazzer report the provided {@link Throwable} as a finding.
+ *
+ * <b>Note:</b> This method must only be called from a method hook. In a
+ * fuzz target, simply throw an exception to trigger a finding.
+ * @param finding the finding that Jazzer should report
+ */
+ public static void reportFindingFromHook(Throwable finding) {
+ try {
+ jazzerInternal.getMethod("reportFindingFromHook", Throwable.class).invoke(null, finding);
+ } catch (NullPointerException | IllegalAccessException | NoSuchMethodException e) {
+ // We can only reach this point if the runtime is not in the classpath, but it must be if
+ // hooks work and this function should only be called from them.
+ System.err.println("ERROR: Jazzer.reportFindingFromHook must be called from a method hook");
+ System.exit(1);
+ } catch (InvocationTargetException e) {
+ // reportFindingFromHook throws a HardToCatchThrowable, which will bubble up wrapped in an
+ // InvocationTargetException that should not be stopped here.
+ if (e.getCause().getClass().getName().endsWith(".HardToCatchError")) {
+ throw(Error) e.getCause();
+ } else {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ // Rethrows a (possibly checked) exception while avoiding a throws declaration.
+ @SuppressWarnings("unchecked")
+ private static <T extends Throwable> void rethrowUnchecked(Throwable t) throws T {
+ throw(T) t;
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java b/agent/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java
new file mode 100644
index 00000000..0d17a4a0
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java
@@ -0,0 +1,183 @@
+// 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.api;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.invoke.MethodType;
+
+/**
+ * Registers this method as a hook that should run after the method
+ * specified by the annotation parameters has returned.
+ * <p>
+ * This method will be called after every call to the target method and has
+ * access to its return value. The target method is specified by
+ * {@link #targetClassName()} and {@link #targetMethod()}. In case of an
+ * overloaded method, {@link #targetMethodDescriptor()} can be used to restrict
+ * the application of the hook to a particular overload.
+ * <p>
+ * The signature of the annotated method must be as follows (this does not
+ * restrict the method name and parameter names, which are arbitrary),
+ * depending on the value of {@link #type()}:
+ *
+ * <dl>
+ * <dt><span class="strong">{@link HookType#BEFORE}</span>
+ * <dd>
+ * <pre>{@code
+ * public static void hook(MethodHandle method, Object thisObject, Object[] arguments, int hookId)
+ * }</pre>
+ * Arguments:
+ * <p><ul>
+ * <li>{@code method}: A {@link java.lang.invoke.MethodHandle} representing the
+ * original method. The original method can be invoked via
+ * {@link java.lang.invoke.MethodHandle#invokeWithArguments(Object...)}. This
+ * requires passing {@code thisObject} as the first argument if the method is
+ * not static. This argument can be {@code null}.
+ * <li>{@code thisObject}: An {@link Object} containing the implicit
+ * {@code this} argument to the original method. If the original method is
+ * static, this argument will be {@code null}.
+ * <li>{@code arguments}: An array of {@link Object}s containing the arguments
+ * passed to the original method. Primitive types (e.g. {@code boolean}) will be
+ * wrapped into their corresponding wrapper type (e.g. {@link Boolean}).
+ * <li>{@code hookId}: A random {@code int} identifying the particular call
+ * site.This can be used to derive additional coverage information.
+ * </ul>
+ *
+ * <dt><span class="strong">{@link HookType#REPLACE}</span>
+ * <dd>
+ * <pre>{@code
+ * public static Object hook(MethodHandle method, Object thisObject, Object[] arguments, int hookId)
+ * }</pre>
+ * The return type may alternatively be taken to be the exact return type of
+ * target method or a wrapper type thereof. The returned object will be casted
+ * and unwrapped automatically.
+ * <p>
+ * Arguments:
+ * <p><ul>
+ * <li>{@code method}: A {@link java.lang.invoke.MethodHandle} representing the
+ * original method. The original method can be invoked via
+ * {@link java.lang.invoke.MethodHandle#invokeWithArguments(Object...)}. This
+ * requires passing {@code thisObject} as the first argument if the method is
+ * not static. This argument can be {@code null}.
+ * <li>{@code thisObject}: An {@link Object} containing the implicit
+ * {@code this} argument to the original method. If the original method is
+ * static, this argument will be {@code null}.
+ * <li>{@code arguments}: An array of {@link Object}s containing the arguments
+ * passed to the original method. Primitive types (e.g. {@code boolean}) will be
+ * wrapped into their corresponding wrapper type (e.g. {@link Boolean}).
+ * <li>{@code hookId}: A random {@code int} identifying the particular call
+ * site.This can be used to derive additional coverage information.
+ * </ul><p>
+ * <p>
+ * Return value: the value that should take the role of the value the target
+ * method would have returned
+ *
+ * <dt><span class="strong">{@link HookType#AFTER}</span>
+ * <dd>
+ * <pre>{@code
+ * public static void hook(MethodHandle method, Object thisObject, Object[] arguments, int hookId,
+ * Object returnValue)
+ * }</pre>
+ * Arguments:
+ * <p><ul>
+ * <li>{@code method}: A {@link java.lang.invoke.MethodHandle} representing the
+ * original method. The original method can be invoked via
+ * {@link java.lang.invoke.MethodHandle#invokeWithArguments(Object...)}. This
+ * requires passing {@code thisObject} as the first argument if the method is
+ * not static. This argument can be {@code null}.
+ * <li>{@code thisObject}: An {@link Object} containing the implicit
+ * {@code this} argument to the original method. If the original method is
+ * static, this argument will be {@code null}.
+ * <li>{@code arguments}: An array of {@link Object}s containing the arguments
+ * passed to the original method. Primitive types (e.g. {@code boolean}) will be
+ * wrapped into their corresponding wrapper type (e.g. {@link Boolean}).
+ * <li>{@code hookId}: A random {@code int} identifying the particular call
+ * site.This can be used to derive additional coverage information.
+ * <li>{@code returnValue}: An {@link Object} containing the return value of the
+ * invocation of the original method. Primitive types (e.g. {@code boolean})
+ * will be wrapped into their corresponding wrapper type (e.g. {@link Boolean}).
+ * If the original method has return type {@code void}, this value will be
+ * {@code null}.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Repeatable(MethodHooks.class)
+@Documented
+public @interface MethodHook {
+ /**
+ * The time at which the annotated method should be called.
+ * <p>
+ * If this is {@link HookType#BEFORE}, the annotated method will be called
+ * before the target method and has access to its arguments.
+ * <p>
+ * If this is {@link HookType#REPLACE}, the annotated method will be called
+ * instead of the target method. It has access to its arguments and can
+ * return a value that will replace the target method's return value.
+ * <p>
+ * If this is {@link HookType#AFTER}, the annotated method will be called
+ * after the target method and has access to its arguments and return
+ * value.
+ *
+ * @return when the hook should be called
+ */
+ HookType type();
+
+ /**
+ * The name of the class that contains the method that should be hooked,
+ * as returned by {@link Class#getName()}.
+ * <p>
+ * Examples:
+ * <p><ul>
+ * <li>{@link String}: {@code "java.lang.String"}
+ * <li>{@link java.nio.file.FileSystem}: {@code "java.nio.file.FileSystem"}
+ * </ul><p>
+ *
+ * @return the name of the class containing the method to be hooked
+ */
+ String targetClassName();
+
+ /**
+ * The name of the method to be hooked. Use {@code "<init>"} for
+ * constructors.
+ * <p>
+ * Examples:
+ * <p><ul>
+ * <li>{@link String#equals(Object)}: {@code "equals"}
+ * <li>{@link String#String()}: {@code "<init>"}
+ * </ul><p>
+ *
+ * @return the name of the method to be hooked
+ */
+ String targetMethod();
+
+ /**
+ * The descriptor of the method to be hooked. This is only needed if there
+ * are multiple methods with the same name and not all of them should be
+ * hooked.
+ * <p>
+ * The descriptor of a method is an internal representation of the method's
+ * signature, which includes the types of its parameters and its return
+ * value. For more information on descriptors, see the
+ * <a href=https://docs.oracle.com/javase/specs/jvms/se15/html/jvms-4.html#jvms-4.3.3>JVM
+ * Specification, Section 4.3.3</a> and {@link MethodType#toMethodDescriptorString()}
+ *
+ * @return the descriptor of the method to be hooked
+ */
+ String targetMethodDescriptor() default "";
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/MethodHooks.java b/agent/src/main/java/com/code_intelligence/jazzer/api/MethodHooks.java
new file mode 100644
index 00000000..7eec24b3
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/api/MethodHooks.java
@@ -0,0 +1,31 @@
+// 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.api;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Internal helper allowing to apply multiple {@link MethodHook} annotations to the same method.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Documented
+public @interface MethodHooks {
+ MethodHook[] value();
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzCodegenVisitor.java b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzCodegenVisitor.java
new file mode 100644
index 00000000..2fbed971
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzCodegenVisitor.java
@@ -0,0 +1,117 @@
+// 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.autofuzz;
+
+import java.util.Stack;
+import java.util.stream.Collectors;
+
+public class AutofuzzCodegenVisitor {
+ private final Stack<Group> groups = new Stack<>();
+ private int variableCounter = 0;
+
+ AutofuzzCodegenVisitor() {
+ init();
+ }
+
+ private void init() {
+ pushGroup("", "", "");
+ }
+
+ public void pushGroup(String prefix, String delimiter, String suffix) {
+ groups.push(new Group(prefix, delimiter, suffix));
+ }
+
+ public void pushElement(String element) {
+ groups.peek().push(element);
+ }
+
+ public void popElement() {
+ groups.peek().pop();
+ }
+
+ public void popGroup() {
+ if (groups.size() == 1) {
+ throw new AutofuzzError(
+ "popGroup must be called exactly once for every pushGroup: " + toDebugString());
+ }
+ pushElement(groups.pop().toString());
+ }
+
+ public String generate() {
+ if (groups.size() != 1) {
+ throw new AutofuzzError(
+ "popGroup must be called exactly once for every pushGroup: " + toDebugString());
+ }
+ return groups.pop().toString();
+ }
+
+ public void addCharLiteral(char c) {
+ pushElement("'" + escapeForLiteral(Character.toString(c)) + "'");
+ }
+
+ public void addStringLiteral(String string) {
+ pushElement('"' + escapeForLiteral(string) + '"');
+ }
+
+ public String uniqueVariableName() {
+ return String.format("autofuzzVariable%s", variableCounter++);
+ }
+
+ private String escapeForLiteral(String string) {
+ // The list of escape sequences is taken from:
+ // https://docs.oracle.com/javase/tutorial/java/data/characters.html
+ return string.replace("\t", "\\t")
+ .replace("\b", "\\b")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ .replace("\f", "\\f")
+ .replace("\f", "\\f")
+ .replace("\"", "\\\"")
+ .replace("'", "\\'")
+ .replace("\\", "\\\\");
+ }
+
+ private String toDebugString() {
+ return groups.stream()
+ .map(group -> group.elements.stream().collect(Collectors.joining(", ", "[", "]")))
+ .collect(Collectors.joining(", ", "[", "]"));
+ }
+
+ private static class Group {
+ private final String prefix;
+ private final String delimiter;
+ private final String suffix;
+ private final Stack<String> elements = new Stack<>();
+
+ Group(String prefix, String delimiter, String suffix) {
+ this.prefix = prefix;
+ this.delimiter = delimiter;
+ this.suffix = suffix;
+ }
+
+ public void push(String element) {
+ elements.push(element);
+ }
+
+ public void pop() {
+ elements.pop();
+ }
+
+ @Override
+ public String toString() {
+ return elements.stream().collect(Collectors.joining(delimiter, prefix, suffix));
+ }
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzError.java b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzError.java
new file mode 100644
index 00000000..a94b385d
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzError.java
@@ -0,0 +1,31 @@
+// 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.autofuzz;
+
+/**
+ * An error indicating an internal error in the autofuzz functionality.
+ */
+public class AutofuzzError extends Error {
+ private static final String MESSAGE_TRAILER = String.format(
+ "%nPlease file an issue at:%n https://github.com/CodeIntelligenceTesting/jazzer/issues/new/choose");
+
+ public AutofuzzError(String message) {
+ super(message + MESSAGE_TRAILER);
+ }
+
+ public AutofuzzError(String message, Throwable cause) {
+ super(message + MESSAGE_TRAILER, cause);
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel
new file mode 100644
index 00000000..779f79cb
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel
@@ -0,0 +1,17 @@
+java_library(
+ name = "autofuzz",
+ srcs = [
+ "AutofuzzCodegenVisitor.java",
+ "AutofuzzError.java",
+ "FuzzTarget.java",
+ "Meta.java",
+ "YourAverageJavaClass.java",
+ ],
+ visibility = ["//visibility:public"],
+ deps = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/api",
+ "//agent/src/main/java/com/code_intelligence/jazzer/utils",
+ "@com_github_classgraph_classgraph//:classgraph",
+ "@com_github_jhalterman_typetools//:typetools",
+ ],
+)
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java
new file mode 100644
index 00000000..8c344621
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java
@@ -0,0 +1,267 @@
+// 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.autofuzz;
+
+import com.code_intelligence.jazzer.api.AutofuzzConstructionException;
+import com.code_intelligence.jazzer.api.AutofuzzInvocationException;
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.utils.SimpleGlobMatcher;
+import com.code_intelligence.jazzer.utils.Utils;
+import java.io.Closeable;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Method;
+import java.net.URLDecoder;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class FuzzTarget {
+ private static final long MAX_EXECUTIONS_WITHOUT_INVOCATION = 100;
+
+ private static String methodReference;
+ private static Executable[] targetExecutables;
+ private static Map<Executable, Class<?>[]> throwsDeclarations;
+ private static Set<SimpleGlobMatcher> ignoredExceptionMatchers;
+ private static long executionsSinceLastInvocation = 0;
+ private static AutofuzzCodegenVisitor codegenVisitor;
+
+ public static void fuzzerInitialize(String[] args) {
+ if (args.length == 0 || !args[0].contains("::")) {
+ System.err.println(
+ "Expected the argument to --autofuzz to be a method reference (e.g. System.out::println)");
+ System.exit(1);
+ }
+ methodReference = args[0];
+ String[] parts = methodReference.split("::", 2);
+ String className = parts[0];
+ String methodNameAndOptionalDescriptor = parts[1];
+ String methodName;
+ String descriptor;
+ int descriptorStart = methodNameAndOptionalDescriptor.indexOf('(');
+ if (descriptorStart != -1) {
+ methodName = methodNameAndOptionalDescriptor.substring(0, descriptorStart);
+ // URL decode the descriptor to allow copy-pasting from javadoc links such as:
+ // https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#valueOf(char%5B%5D)
+ try {
+ descriptor =
+ URLDecoder.decode(methodNameAndOptionalDescriptor.substring(descriptorStart), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // UTF-8 is always supported.
+ e.printStackTrace();
+ System.exit(1);
+ return;
+ }
+ } else {
+ methodName = methodNameAndOptionalDescriptor;
+ descriptor = null;
+ }
+
+ Class<?> targetClass;
+ try {
+ // Explicitly invoking static initializers to trigger some coverage in the code.
+ targetClass = Class.forName(className, true, ClassLoader.getSystemClassLoader());
+ } catch (ClassNotFoundException e) {
+ System.err.printf(
+ "Failed to find class %s for autofuzz, please ensure it is contained in the classpath "
+ + "specified with --cp and specify the full package name%n",
+ className);
+ e.printStackTrace();
+ System.exit(1);
+ return;
+ }
+
+ boolean isConstructor = methodName.equals("new");
+ if (isConstructor) {
+ targetExecutables =
+ Arrays.stream(targetClass.getConstructors())
+ .filter(constructor
+ -> descriptor == null
+ || Utils.getReadableDescriptor(constructor).equals(descriptor))
+ .toArray(Executable[] ::new);
+ } else {
+ targetExecutables =
+ Arrays.stream(targetClass.getMethods())
+ .filter(method
+ -> method.getName().equals(methodName)
+ && (descriptor == null
+ || Utils.getReadableDescriptor(method).equals(descriptor)))
+ .toArray(Executable[] ::new);
+ }
+ if (targetExecutables.length == 0) {
+ if (isConstructor) {
+ if (descriptor == null) {
+ System.err.printf(
+ "Failed to find accessible constructors in class %s for autofuzz.%n", className);
+ } else {
+ System.err.printf(
+ "Failed to find accessible constructors with signature %s in class %s for autofuzz.%n"
+ + "Accessible constructors:%n%s",
+ descriptor, className,
+ Arrays.stream(targetClass.getConstructors())
+ .map(method
+ -> String.format("%s::new%s", method.getDeclaringClass().getName(),
+ Utils.getReadableDescriptor(method)))
+ .distinct()
+ .collect(Collectors.joining(System.lineSeparator())));
+ }
+ } else {
+ if (descriptor == null) {
+ System.err.printf("Failed to find accessible methods named %s in class %s for autofuzz.%n"
+ + "Accessible methods:%n%s",
+ methodName, className,
+ Arrays.stream(targetClass.getMethods())
+ .map(method
+ -> String.format(
+ "%s::%s", method.getDeclaringClass().getName(), method.getName()))
+ .distinct()
+ .collect(Collectors.joining(System.lineSeparator())));
+ } else {
+ System.err.printf(
+ "Failed to find accessible methods named %s with signature %s in class %s for autofuzz.%n"
+ + "Accessible methods with that name:%n%s",
+ methodName, descriptor, className,
+ Arrays.stream(targetClass.getMethods())
+ .filter(method -> method.getName().equals(methodName))
+ .map(method
+ -> String.format("%s::%s%s", method.getDeclaringClass().getName(),
+ method.getName(), Utils.getReadableDescriptor(method)))
+ .distinct()
+ .collect(Collectors.joining(System.lineSeparator())));
+ }
+ }
+ System.exit(1);
+ }
+
+ ignoredExceptionMatchers = Arrays.stream(args)
+ .skip(1)
+ .filter(s -> s.contains("*"))
+ .map(SimpleGlobMatcher::new)
+ .collect(Collectors.toSet());
+
+ List<Class<?>> alwaysIgnore =
+ Arrays.stream(args)
+ .skip(1)
+ .filter(s -> !s.contains("*"))
+ .map(name -> {
+ try {
+ return ClassLoader.getSystemClassLoader().loadClass(name);
+ } catch (ClassNotFoundException e) {
+ System.err.printf("Failed to find class '%s' specified in --autofuzz_ignore", name);
+ System.exit(1);
+ }
+ throw new Error("Not reached");
+ })
+ .collect(Collectors.toList());
+ throwsDeclarations =
+ Arrays.stream(targetExecutables)
+ .collect(Collectors.toMap(method
+ -> method,
+ method
+ -> Stream.concat(Arrays.stream(method.getExceptionTypes()), alwaysIgnore.stream())
+ .toArray(Class[] ::new)));
+ }
+
+ public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Throwable {
+ if (Meta.isDebug()) {
+ codegenVisitor = new AutofuzzCodegenVisitor();
+ }
+ Executable targetExecutable;
+ if (FuzzTarget.targetExecutables.length == 1) {
+ targetExecutable = FuzzTarget.targetExecutables[0];
+ } else {
+ targetExecutable = data.pickValue(FuzzTarget.targetExecutables);
+ }
+ Object returnValue = null;
+ try {
+ if (targetExecutable instanceof Method) {
+ returnValue = Meta.autofuzz(data, (Method) targetExecutable, codegenVisitor);
+ } else {
+ returnValue = Meta.autofuzz(data, (Constructor<?>) targetExecutable, codegenVisitor);
+ }
+ executionsSinceLastInvocation = 0;
+ if (codegenVisitor != null) {
+ System.err.println(codegenVisitor.generate());
+ }
+ } catch (AutofuzzConstructionException e) {
+ if (Meta.isDebug()) {
+ e.printStackTrace();
+ }
+ // Ignore exceptions thrown while constructing the parameters for the target method. We can
+ // only guess how to generate valid parameters and any exceptions thrown while doing so
+ // are most likely on us. However, if this happens too often, Autofuzz got stuck and we should
+ // let the user know.
+ executionsSinceLastInvocation++;
+ if (executionsSinceLastInvocation >= MAX_EXECUTIONS_WITHOUT_INVOCATION) {
+ System.err.printf("Failed to generate valid arguments to '%s' in %d attempts; giving up%n",
+ methodReference, executionsSinceLastInvocation);
+ System.exit(1);
+ } else if (executionsSinceLastInvocation == MAX_EXECUTIONS_WITHOUT_INVOCATION / 2) {
+ // The application under test might perform classpath modifications or create classes
+ // dynamically that implement interfaces or extend abstract classes. Rescanning the
+ // classpath might help with constructing objects.
+ Meta.rescanClasspath();
+ }
+ } catch (AutofuzzInvocationException e) {
+ executionsSinceLastInvocation = 0;
+ Throwable cause = e.getCause();
+ Class<?> causeClass = cause.getClass();
+ // Do not report exceptions declared to be thrown by the method under test.
+ for (Class<?> declaredThrow : throwsDeclarations.get(targetExecutable)) {
+ if (declaredThrow.isAssignableFrom(causeClass)) {
+ return;
+ }
+ }
+
+ if (ignoredExceptionMatchers.stream().anyMatch(m -> m.matches(causeClass.getName()))) {
+ return;
+ }
+ cleanStackTraces(cause);
+ throw cause;
+ } catch (Throwable t) {
+ System.err.println("Unexpected exception encountered during autofuzz");
+ t.printStackTrace();
+ System.exit(1);
+ } finally {
+ if (returnValue instanceof Closeable) {
+ ((Closeable) returnValue).close();
+ }
+ }
+ }
+
+ // Removes all stack trace elements that live in the Java standard library, internal JDK classes
+ // or the autofuzz package from the bottom of all stack frames.
+ private static void cleanStackTraces(Throwable t) {
+ Throwable cause = t;
+ while (cause != null) {
+ StackTraceElement[] elements = cause.getStackTrace();
+ int firstInterestingPos;
+ for (firstInterestingPos = elements.length - 1; firstInterestingPos > 0;
+ firstInterestingPos--) {
+ String className = elements[firstInterestingPos].getClassName();
+ if (!className.startsWith("com.code_intelligence.jazzer.autofuzz")
+ && !className.startsWith("java.") && !className.startsWith("jdk.")) {
+ break;
+ }
+ }
+ cause.setStackTrace(Arrays.copyOfRange(elements, 0, firstInterestingPos + 1));
+ cause = cause.getCause();
+ }
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java
new file mode 100644
index 00000000..96980530
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java
@@ -0,0 +1,619 @@
+// 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.autofuzz;
+
+import com.code_intelligence.jazzer.api.AutofuzzConstructionException;
+import com.code_intelligence.jazzer.api.AutofuzzInvocationException;
+import com.code_intelligence.jazzer.api.Consumer1;
+import com.code_intelligence.jazzer.api.Consumer2;
+import com.code_intelligence.jazzer.api.Consumer3;
+import com.code_intelligence.jazzer.api.Consumer4;
+import com.code_intelligence.jazzer.api.Consumer5;
+import com.code_intelligence.jazzer.api.Function1;
+import com.code_intelligence.jazzer.api.Function2;
+import com.code_intelligence.jazzer.api.Function3;
+import com.code_intelligence.jazzer.api.Function4;
+import com.code_intelligence.jazzer.api.Function5;
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.utils.Utils;
+import io.github.classgraph.ClassGraph;
+import io.github.classgraph.ClassInfoList;
+import io.github.classgraph.ScanResult;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import net.jodah.typetools.TypeResolver;
+import net.jodah.typetools.TypeResolver.Unknown;
+
+public class Meta {
+ static WeakHashMap<Class<?>, List<Class<?>>> implementingClassesCache = new WeakHashMap<>();
+ static WeakHashMap<Class<?>, List<Class<?>>> nestedBuilderClassesCache = new WeakHashMap<>();
+ static WeakHashMap<Class<?>, List<Method>> originalObjectCreationMethodsCache =
+ new WeakHashMap<>();
+ static WeakHashMap<Class<?>, List<Method>> cascadingBuilderMethodsCache = new WeakHashMap<>();
+
+ public static Object autofuzz(FuzzedDataProvider data, Method method) {
+ return autofuzz(data, method, null);
+ }
+
+ static Object autofuzz(FuzzedDataProvider data, Method method, AutofuzzCodegenVisitor visitor) {
+ Object result;
+ if (Modifier.isStatic(method.getModifiers())) {
+ if (visitor != null) {
+ // This group will always have two elements: The class name and the method call.
+ visitor.pushGroup(
+ String.format("%s.", method.getDeclaringClass().getCanonicalName()), "", "");
+ }
+ result = autofuzz(data, method, null, visitor);
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ } else {
+ if (visitor != null) {
+ // This group will always have two elements: The thisObject and the method call.
+ visitor.pushGroup("", ".", "");
+ }
+ Object thisObject = consume(data, method.getDeclaringClass(), visitor);
+ if (thisObject == null) {
+ throw new AutofuzzConstructionException();
+ }
+ result = autofuzz(data, method, thisObject, visitor);
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ }
+ return result;
+ }
+
+ public static Object autofuzz(FuzzedDataProvider data, Method method, Object thisObject) {
+ return autofuzz(data, method, thisObject, null);
+ }
+
+ static Object autofuzz(
+ FuzzedDataProvider data, Method method, Object thisObject, AutofuzzCodegenVisitor visitor) {
+ if (visitor != null) {
+ visitor.pushGroup(String.format("%s(", method.getName()), ", ", ")");
+ }
+ Object[] arguments = consumeArguments(data, method, visitor);
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ try {
+ return method.invoke(thisObject, arguments);
+ } catch (IllegalAccessException | IllegalArgumentException | NullPointerException e) {
+ // We should ensure that the arguments fed into the method are always valid.
+ throw new AutofuzzError(getDebugSummary(method, thisObject, arguments), e);
+ } catch (InvocationTargetException e) {
+ throw new AutofuzzInvocationException(e.getCause());
+ }
+ }
+
+ public static <R> R autofuzz(FuzzedDataProvider data, Constructor<R> constructor) {
+ return autofuzz(data, constructor, null);
+ }
+
+ static <R> R autofuzz(
+ FuzzedDataProvider data, Constructor<R> constructor, AutofuzzCodegenVisitor visitor) {
+ if (visitor != null) {
+ // getCanonicalName is correct also for nested classes.
+ visitor.pushGroup(
+ String.format("new %s(", constructor.getDeclaringClass().getCanonicalName()), ", ", ")");
+ }
+ Object[] arguments = consumeArguments(data, constructor, visitor);
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ try {
+ return constructor.newInstance(arguments);
+ } catch (InstantiationException | IllegalAccessException | IllegalArgumentException e) {
+ // This should never be reached as the logic in consume should prevent us from e.g. calling
+ // constructors of abstract classes or private constructors.
+ throw new AutofuzzError(getDebugSummary(constructor, null, arguments), e);
+ } catch (InvocationTargetException e) {
+ throw new AutofuzzInvocationException(e.getCause());
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1> void autofuzz(FuzzedDataProvider data, Consumer1<T1> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Consumer1.class, func.getClass());
+ func.accept((T1) consumeChecked(data, types, 0));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2> void autofuzz(FuzzedDataProvider data, Consumer2<T1, T2> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Consumer2.class, func.getClass());
+ func.accept((T1) consumeChecked(data, types, 0), (T2) consumeChecked(data, types, 1));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3> void autofuzz(FuzzedDataProvider data, Consumer3<T1, T2, T3> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Consumer3.class, func.getClass());
+ func.accept((T1) consumeChecked(data, types, 0), (T2) consumeChecked(data, types, 1),
+ (T3) consumeChecked(data, types, 2));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, T4> void autofuzz(
+ FuzzedDataProvider data, Consumer4<T1, T2, T3, T4> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Consumer4.class, func.getClass());
+ func.accept((T1) consumeChecked(data, types, 0), (T2) consumeChecked(data, types, 1),
+ (T3) consumeChecked(data, types, 2), (T4) consumeChecked(data, types, 3));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, T4, T5> void autofuzz(
+ FuzzedDataProvider data, Consumer5<T1, T2, T3, T4, T5> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Consumer5.class, func.getClass());
+ func.accept((T1) consumeChecked(data, types, 0), (T2) consumeChecked(data, types, 1),
+ (T3) consumeChecked(data, types, 2), (T4) consumeChecked(data, types, 3),
+ (T5) consumeChecked(data, types, 4));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, R> R autofuzz(FuzzedDataProvider data, Function1<T1, R> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Function1.class, func.getClass());
+ return func.apply((T1) consumeChecked(data, types, 0));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, R> R autofuzz(FuzzedDataProvider data, Function2<T1, T2, R> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Function2.class, func.getClass());
+ return func.apply((T1) consumeChecked(data, types, 0), (T2) consumeChecked(data, types, 1));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, R> R autofuzz(FuzzedDataProvider data, Function3<T1, T2, T3, R> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Function3.class, func.getClass());
+ return func.apply((T1) consumeChecked(data, types, 0), (T2) consumeChecked(data, types, 1),
+ (T3) consumeChecked(data, types, 2));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, T4, R> R autofuzz(
+ FuzzedDataProvider data, Function4<T1, T2, T3, T4, R> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Function4.class, func.getClass());
+ return func.apply((T1) consumeChecked(data, types, 0), (T2) consumeChecked(data, types, 1),
+ (T3) consumeChecked(data, types, 2), (T4) consumeChecked(data, types, 3));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T1, T2, T3, T4, T5, R> R autofuzz(
+ FuzzedDataProvider data, Function5<T1, T2, T3, T4, T5, R> func) {
+ Class<?>[] types = TypeResolver.resolveRawArguments(Function5.class, func.getClass());
+ return func.apply((T1) consumeChecked(data, types, 0), (T2) consumeChecked(data, types, 1),
+ (T3) consumeChecked(data, types, 2), (T4) consumeChecked(data, types, 3),
+ (T5) consumeChecked(data, types, 4));
+ }
+
+ public static Object consume(FuzzedDataProvider data, Class<?> type) {
+ return consume(data, type, null);
+ }
+
+ static Object consume(FuzzedDataProvider data, Class<?> type, AutofuzzCodegenVisitor visitor) {
+ if (type == byte.class || type == Byte.class) {
+ byte result = data.consumeByte();
+ if (visitor != null)
+ visitor.pushElement(String.format("(byte) %s", result));
+ return result;
+ } else if (type == short.class || type == Short.class) {
+ short result = data.consumeShort();
+ if (visitor != null)
+ visitor.pushElement(String.format("(short) %s", result));
+ return result;
+ } else if (type == int.class || type == Integer.class) {
+ int result = data.consumeInt();
+ if (visitor != null)
+ visitor.pushElement(Integer.toString(result));
+ return result;
+ } else if (type == long.class || type == Long.class) {
+ long result = data.consumeLong();
+ if (visitor != null)
+ visitor.pushElement(String.format("%sL", result));
+ return result;
+ } else if (type == float.class || type == Float.class) {
+ float result = data.consumeFloat();
+ if (visitor != null)
+ visitor.pushElement(String.format("%sF", result));
+ return result;
+ } else if (type == double.class || type == Double.class) {
+ double result = data.consumeDouble();
+ if (visitor != null)
+ visitor.pushElement(Double.toString(result));
+ return result;
+ } else if (type == boolean.class || type == Boolean.class) {
+ boolean result = data.consumeBoolean();
+ if (visitor != null)
+ visitor.pushElement(Boolean.toString(result));
+ return result;
+ } else if (type == char.class || type == Character.class) {
+ char result = data.consumeChar();
+ if (visitor != null)
+ visitor.addCharLiteral(result);
+ return result;
+ }
+ // Return null for non-primitive and non-boxed types in ~5% of the cases.
+ // TODO: We might want to return null for boxed types sometimes, but this is complicated by the
+ // fact that TypeUtils can't distinguish between a primitive type and its wrapper and may
+ // thus easily cause false-positive NullPointerExceptions.
+ if (!type.isPrimitive() && data.consumeByte((byte) 0, (byte) 19) == 0) {
+ if (visitor != null)
+ visitor.pushElement("null");
+ return null;
+ }
+ if (type == String.class || type == CharSequence.class) {
+ String result = data.consumeString(consumeArrayLength(data, 1));
+ if (visitor != null)
+ visitor.addStringLiteral(result);
+ return result;
+ } else if (type.isArray()) {
+ if (type == byte[].class) {
+ byte[] result = data.consumeBytes(consumeArrayLength(data, Byte.BYTES));
+ if (visitor != null) {
+ visitor.pushElement(IntStream.range(0, result.length)
+ .mapToObj(i -> "(byte) " + result[i])
+ .collect(Collectors.joining(", ", "new byte[]{", "}")));
+ }
+ return result;
+ } else if (type == int[].class) {
+ int[] result = data.consumeInts(consumeArrayLength(data, Integer.BYTES));
+ if (visitor != null) {
+ visitor.pushElement(Arrays.stream(result)
+ .mapToObj(String::valueOf)
+ .collect(Collectors.joining(", ", "new int[]{", "}")));
+ }
+ return result;
+ } else if (type == short[].class) {
+ short[] result = data.consumeShorts(consumeArrayLength(data, Short.BYTES));
+ if (visitor != null) {
+ visitor.pushElement(IntStream.range(0, result.length)
+ .mapToObj(i -> "(short) " + result[i])
+ .collect(Collectors.joining(", ", "new short[]{", "}")));
+ }
+ return result;
+ } else if (type == long[].class) {
+ long[] result = data.consumeLongs(consumeArrayLength(data, Long.BYTES));
+ if (visitor != null) {
+ visitor.pushElement(Arrays.stream(result)
+ .mapToObj(e -> e + "L")
+ .collect(Collectors.joining(", ", "new long[]{", "}")));
+ }
+ return result;
+ } else if (type == boolean[].class) {
+ boolean[] result = data.consumeBooleans(consumeArrayLength(data, 1));
+ if (visitor != null) {
+ visitor.pushElement(
+ Arrays.toString(result).replace(']', '}').replace("[", "new boolean[]{"));
+ }
+ return result;
+ } else {
+ if (visitor != null) {
+ visitor.pushGroup(
+ String.format("new %s[]{", type.getComponentType().getName()), ", ", "}");
+ }
+ int remainingBytesBeforeFirstElementCreation = data.remainingBytes();
+ Object firstElement = consume(data, type.getComponentType(), visitor);
+ int remainingBytesAfterFirstElementCreation = data.remainingBytes();
+ int sizeOfElementEstimate =
+ remainingBytesBeforeFirstElementCreation - remainingBytesAfterFirstElementCreation;
+ Object array = Array.newInstance(
+ type.getComponentType(), consumeArrayLength(data, sizeOfElementEstimate));
+ for (int i = 0; i < Array.getLength(array); i++) {
+ if (i == 0) {
+ Array.set(array, i, firstElement);
+ } else {
+ Array.set(array, i, consume(data, type.getComponentType(), visitor));
+ }
+ }
+ if (visitor != null) {
+ if (Array.getLength(array) == 0) {
+ // We implicitly pushed the first element with the call to consume above, but it is not
+ // part of the array.
+ visitor.popElement();
+ }
+ visitor.popGroup();
+ }
+ return array;
+ }
+ } else if (type == ByteArrayInputStream.class || type == InputStream.class) {
+ byte[] array = data.consumeBytes(consumeArrayLength(data, Byte.BYTES));
+ if (visitor != null) {
+ visitor.pushElement(IntStream.range(0, array.length)
+ .mapToObj(i -> "(byte) " + array[i])
+ .collect(Collectors.joining(
+ ", ", "new java.io.ByteArrayInputStream(new byte[]{", "})")));
+ }
+ return new ByteArrayInputStream(array);
+ } else if (type.isEnum()) {
+ Enum<?> enumValue = (Enum<?>) data.pickValue(type.getEnumConstants());
+ if (visitor != null) {
+ visitor.pushElement(String.format("%s.%s", type.getName(), enumValue.name()));
+ }
+ return enumValue;
+ } else if (type == Class.class) {
+ if (visitor != null)
+ visitor.pushElement(String.format("%s.class", YourAverageJavaClass.class.getName()));
+ return YourAverageJavaClass.class;
+ } else if (type == Method.class) {
+ if (visitor != null) {
+ throw new AutofuzzError("codegen has not been implemented for Method.class");
+ }
+ return data.pickValue(sortExecutables(YourAverageJavaClass.class.getMethods()));
+ } else if (type == Constructor.class) {
+ if (visitor != null) {
+ throw new AutofuzzError("codegen has not been implemented for Constructor.class");
+ }
+ return data.pickValue(sortExecutables(YourAverageJavaClass.class.getConstructors()));
+ } else if (type.isInterface() || Modifier.isAbstract(type.getModifiers())) {
+ List<Class<?>> implementingClasses = implementingClassesCache.get(type);
+ if (implementingClasses == null) {
+ ClassGraph classGraph =
+ new ClassGraph().enableClassInfo().enableInterClassDependencies().rejectPackages(
+ "jaz.*");
+ if (!isTest()) {
+ classGraph.rejectPackages("com.code_intelligence.jazzer.*");
+ }
+ try (ScanResult result = classGraph.scan()) {
+ ClassInfoList children =
+ type.isInterface() ? result.getClassesImplementing(type) : result.getSubclasses(type);
+ implementingClasses =
+ children.getStandardClasses().filter(cls -> !cls.isAbstract()).loadClasses();
+ implementingClassesCache.put(type, implementingClasses);
+ }
+ }
+ if (implementingClasses.isEmpty()) {
+ if (isDebug()) {
+ throw new AutofuzzConstructionException(String.format(
+ "Could not find classes implementing %s on the classpath", type.getName()));
+ } else {
+ throw new AutofuzzConstructionException();
+ }
+ }
+ if (visitor != null) {
+ // This group will always have a single element: The instance of the implementing class.
+ visitor.pushGroup(String.format("(%s) ", type.getName()), "", "");
+ }
+ Object result = consume(data, data.pickValue(implementingClasses), visitor);
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ return result;
+ } else if (type.getConstructors().length > 0) {
+ Constructor<?> constructor = data.pickValue(sortExecutables(type.getConstructors()));
+ boolean applySetters = constructor.getParameterCount() == 0;
+ if (visitor != null && applySetters) {
+ // Embed the instance creation and setters into an immediately invoked lambda expression to
+ // turn them into an expression.
+ String uniqueVariableName = visitor.uniqueVariableName();
+ visitor.pushGroup(String.format("((java.util.function.Supplier<%1$s>) (() -> {%1$s %2$s = ",
+ type.getCanonicalName(), uniqueVariableName),
+ String.format("; %s.", uniqueVariableName),
+ String.format("; return %s;})).get()", uniqueVariableName));
+ }
+ Object obj = autofuzz(data, constructor, visitor);
+ if (applySetters) {
+ List<Method> potentialSetters = getPotentialSetters(type);
+ if (!potentialSetters.isEmpty()) {
+ List<Method> pickedSetters =
+ data.pickValues(potentialSetters, data.consumeInt(0, potentialSetters.size()));
+ for (Method setter : pickedSetters) {
+ autofuzz(data, setter, obj, visitor);
+ }
+ }
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ }
+ return obj;
+ }
+ // We are out of more or less canonical ways to construct an instance of this class and have to
+ // resort to more heuristic approaches.
+
+ // First, try to find nested classes with names ending in Builder and call a subset of their
+ // chaining methods.
+ List<Class<?>> nestedBuilderClasses = getNestedBuilderClasses(type);
+ if (!nestedBuilderClasses.isEmpty()) {
+ Class<?> pickedBuilder = data.pickValue(nestedBuilderClasses);
+ List<Method> cascadingBuilderMethods = getCascadingBuilderMethods(pickedBuilder);
+ List<Method> originalObjectCreationMethods = getOriginalObjectCreationMethods(pickedBuilder);
+
+ int pickedMethodsNumber = data.consumeInt(0, cascadingBuilderMethods.size());
+ List<Method> pickedMethods = data.pickValues(cascadingBuilderMethods, pickedMethodsNumber);
+ Method builderMethod = data.pickValue(originalObjectCreationMethods);
+
+ if (visitor != null) {
+ // Group for the chain of builder methods.
+ visitor.pushGroup("", ".", "");
+ }
+ Object builderObj =
+ autofuzz(data, data.pickValue(sortExecutables(pickedBuilder.getConstructors())), visitor);
+ for (Method method : pickedMethods) {
+ builderObj = autofuzz(data, method, builderObj, visitor);
+ }
+
+ try {
+ Object obj = autofuzz(data, builderMethod, builderObj, visitor);
+ if (visitor != null) {
+ visitor.popGroup();
+ }
+ return obj;
+ } catch (Exception e) {
+ throw new AutofuzzConstructionException(e);
+ }
+ }
+
+ // We ran out of ways to construct an instance of the requested type. If in debug mode, report
+ // more detailed information.
+ if (!isDebug()) {
+ throw new AutofuzzConstructionException();
+ } else {
+ String summary = String.format(
+ "Failed to generate instance of %s:%nAccessible constructors: %s%nNested subclasses: %s%n",
+ type.getName(),
+ Arrays.stream(type.getConstructors())
+ .map(Utils::getReadableDescriptor)
+ .collect(Collectors.joining(", ")),
+ Arrays.stream(type.getClasses()).map(Class::getName).collect(Collectors.joining(", ")));
+ throw new AutofuzzConstructionException(summary);
+ }
+ }
+
+ static void rescanClasspath() {
+ implementingClassesCache.clear();
+ }
+
+ static boolean isTest() {
+ String value = System.getenv("JAZZER_AUTOFUZZ_TESTING");
+ return value != null && !value.isEmpty();
+ }
+
+ static boolean isDebug() {
+ String value = System.getenv("JAZZER_AUTOFUZZ_DEBUG");
+ return value != null && !value.isEmpty();
+ }
+
+ private static int consumeArrayLength(FuzzedDataProvider data, int sizeOfElement) {
+ // Spend at most half of the fuzzer input bytes so that the remaining arguments that require
+ // construction still have non-trivial data to work with.
+ int bytesToSpend = data.remainingBytes() / 2;
+ return bytesToSpend / Math.max(sizeOfElement, 1);
+ }
+
+ private static String getDebugSummary(
+ Executable executable, Object thisObject, Object[] arguments) {
+ return String.format("%nMethod: %s::%s%s%nthis: %s%nArguments: %s",
+ executable.getDeclaringClass().getName(), executable.getName(),
+ Utils.getReadableDescriptor(executable), thisObject,
+ Arrays.stream(arguments)
+ .map(arg -> arg == null ? "null" : arg.toString())
+ .collect(Collectors.joining(", ")));
+ }
+
+ private static <T extends Executable> List<T> sortExecutables(T[] executables) {
+ List<T> list = Arrays.asList(executables);
+ sortExecutables(list);
+ return list;
+ }
+
+ private static void sortExecutables(List<? extends Executable> executables) {
+ executables.sort(Comparator.comparing(Executable::getName).thenComparing(Utils::getDescriptor));
+ }
+
+ private static void sortClasses(List<? extends Class<?>> classes) {
+ classes.sort(Comparator.comparing(Class::getName));
+ }
+
+ private static List<Class<?>> getNestedBuilderClasses(Class<?> type) {
+ List<Class<?>> nestedBuilderClasses = nestedBuilderClassesCache.get(type);
+ if (nestedBuilderClasses == null) {
+ nestedBuilderClasses = Arrays.stream(type.getClasses())
+ .filter(cls -> cls.getName().endsWith("Builder"))
+ .filter(cls -> !getOriginalObjectCreationMethods(cls).isEmpty())
+ .collect(Collectors.toList());
+ sortClasses(nestedBuilderClasses);
+ nestedBuilderClassesCache.put(type, nestedBuilderClasses);
+ }
+ return nestedBuilderClasses;
+ }
+
+ private static List<Method> getOriginalObjectCreationMethods(Class<?> builder) {
+ List<Method> originalObjectCreationMethods = originalObjectCreationMethodsCache.get(builder);
+ if (originalObjectCreationMethods == null) {
+ originalObjectCreationMethods =
+ Arrays.stream(builder.getMethods())
+ .filter(m -> m.getReturnType() == builder.getEnclosingClass())
+ .collect(Collectors.toList());
+ sortExecutables(originalObjectCreationMethods);
+ originalObjectCreationMethodsCache.put(builder, originalObjectCreationMethods);
+ }
+ return originalObjectCreationMethods;
+ }
+
+ private static List<Method> getCascadingBuilderMethods(Class<?> builder) {
+ List<Method> cascadingBuilderMethods = cascadingBuilderMethodsCache.get(builder);
+ if (cascadingBuilderMethods == null) {
+ cascadingBuilderMethods = Arrays.stream(builder.getMethods())
+ .filter(m -> m.getReturnType() == builder)
+ .collect(Collectors.toList());
+ sortExecutables(cascadingBuilderMethods);
+ cascadingBuilderMethodsCache.put(builder, cascadingBuilderMethods);
+ }
+ return cascadingBuilderMethods;
+ }
+
+ private static List<Method> getPotentialSetters(Class<?> type) {
+ List<Method> potentialSetters = new ArrayList<>();
+ Method[] methods = type.getMethods();
+ for (Method method : methods) {
+ if (void.class.equals(method.getReturnType()) && method.getParameterCount() == 1
+ && method.getName().startsWith("set")) {
+ potentialSetters.add(method);
+ }
+ }
+ sortExecutables(potentialSetters);
+ return potentialSetters;
+ }
+
+ private static Object[] consumeArguments(
+ FuzzedDataProvider data, Executable executable, AutofuzzCodegenVisitor visitor) {
+ Object[] result;
+ try {
+ result = Arrays.stream(executable.getParameterTypes())
+ .map((type) -> consume(data, type, visitor))
+ .toArray();
+ return result;
+ } catch (AutofuzzConstructionException e) {
+ // Do not nest AutofuzzConstructionExceptions.
+ throw e;
+ } catch (AutofuzzInvocationException e) {
+ // If an invocation fails while creating the arguments for another invocation, the exception
+ // should not be reported, so we rewrap it.
+ throw new AutofuzzConstructionException(e.getCause());
+ } catch (Throwable t) {
+ throw new AutofuzzConstructionException(t);
+ }
+ }
+
+ private static Object consumeChecked(FuzzedDataProvider data, Class<?>[] types, int i) {
+ if (types[i] == Unknown.class) {
+ throw new AutofuzzError("Failed to determine type of argument " + (i + 1));
+ }
+ Object result;
+ try {
+ result = consume(data, types[i]);
+ } catch (AutofuzzConstructionException e) {
+ // Do not nest AutofuzzConstructionExceptions.
+ throw e;
+ } catch (AutofuzzInvocationException e) {
+ // If an invocation fails while creating the arguments for another invocation, the exception
+ // should not be reported, so we rewrap it.
+ throw new AutofuzzConstructionException(e.getCause());
+ } catch (Throwable t) {
+ throw new AutofuzzConstructionException(t);
+ }
+ if (result != null && !types[i].isAssignableFrom(result.getClass())) {
+ throw new AutofuzzError("consume returned " + result.getClass() + ", but need " + types[i]);
+ }
+ return result;
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/YourAverageJavaClass.java b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/YourAverageJavaClass.java
new file mode 100644
index 00000000..452ca878
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/YourAverageJavaClass.java
@@ -0,0 +1,229 @@
+// 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.autofuzz;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+// Returned by Meta when asked to construct a Class object. Its purpose is to be a relatively
+// "interesting" Java data class that can serve as the target of methods that perform some kind of
+// reflection or deserialization.
+public class YourAverageJavaClass implements Cloneable, Closeable, Serializable {
+ public byte aByte;
+ public boolean aBoolean;
+ public double aDouble;
+ public float aFloat;
+ public int anInt;
+ public transient int transientInt;
+ public long aLong;
+ public short aShort;
+ public volatile short volatileShort;
+ public String string;
+ public byte[] bytes;
+ public List<YourAverageJavaClass> list;
+ public Map<String, YourAverageJavaClass> map;
+
+ // Everything below has been automatically generated (apart from a minor modification to clone());
+
+ public YourAverageJavaClass(byte aByte, boolean aBoolean, double aDouble, float aFloat, int anInt,
+ int transientInt, long aLong, short aShort, short volatileShort, String string) {
+ this.aByte = aByte;
+ this.aBoolean = aBoolean;
+ this.aDouble = aDouble;
+ this.aFloat = aFloat;
+ this.anInt = anInt;
+ this.transientInt = transientInt;
+ this.aLong = aLong;
+ this.aShort = aShort;
+ this.volatileShort = volatileShort;
+ this.string = string;
+ }
+
+ public YourAverageJavaClass() {}
+
+ public YourAverageJavaClass(byte aByte, boolean aBoolean, double aDouble, float aFloat, int anInt,
+ int transientInt, long aLong, short aShort, short volatileShort, String string, byte[] bytes,
+ List<YourAverageJavaClass> list, Map<String, YourAverageJavaClass> map) {
+ this.aByte = aByte;
+ this.aBoolean = aBoolean;
+ this.aDouble = aDouble;
+ this.aFloat = aFloat;
+ this.anInt = anInt;
+ this.transientInt = transientInt;
+ this.aLong = aLong;
+ this.aShort = aShort;
+ this.volatileShort = volatileShort;
+ this.string = string;
+ this.bytes = bytes;
+ this.list = list;
+ this.map = map;
+ }
+
+ public byte getaByte() {
+ return aByte;
+ }
+
+ public void setaByte(byte aByte) {
+ this.aByte = aByte;
+ }
+
+ public boolean isaBoolean() {
+ return aBoolean;
+ }
+
+ public void setaBoolean(boolean aBoolean) {
+ this.aBoolean = aBoolean;
+ }
+
+ public double getaDouble() {
+ return aDouble;
+ }
+
+ public void setaDouble(double aDouble) {
+ this.aDouble = aDouble;
+ }
+
+ public float getaFloat() {
+ return aFloat;
+ }
+
+ public void setaFloat(float aFloat) {
+ this.aFloat = aFloat;
+ }
+
+ public int getAnInt() {
+ return anInt;
+ }
+
+ public void setAnInt(int anInt) {
+ this.anInt = anInt;
+ }
+
+ public int getTransientInt() {
+ return transientInt;
+ }
+
+ public void setTransientInt(int transientInt) {
+ this.transientInt = transientInt;
+ }
+
+ public long getaLong() {
+ return aLong;
+ }
+
+ public void setaLong(long aLong) {
+ this.aLong = aLong;
+ }
+
+ public short getaShort() {
+ return aShort;
+ }
+
+ public void setaShort(short aShort) {
+ this.aShort = aShort;
+ }
+
+ public short getVolatileShort() {
+ return volatileShort;
+ }
+
+ public void setVolatileShort(short volatileShort) {
+ this.volatileShort = volatileShort;
+ }
+
+ public String getString() {
+ return string;
+ }
+
+ public void setString(String string) {
+ this.string = string;
+ }
+
+ public byte[] getBytes() {
+ return bytes;
+ }
+
+ public void setBytes(byte[] bytes) {
+ this.bytes = bytes;
+ }
+
+ public List<YourAverageJavaClass> getList() {
+ return list;
+ }
+
+ public void setList(List<YourAverageJavaClass> list) {
+ this.list = list;
+ }
+
+ public Map<String, YourAverageJavaClass> getMap() {
+ return map;
+ }
+
+ public void setMap(Map<String, YourAverageJavaClass> map) {
+ this.map = map;
+ }
+
+ @Override
+ public YourAverageJavaClass clone() {
+ try {
+ YourAverageJavaClass clone = (YourAverageJavaClass) super.clone();
+ clone.transientInt = transientInt + 1;
+ clone.volatileShort = (short) (volatileShort - 1);
+ return clone;
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (!(o instanceof YourAverageJavaClass))
+ return false;
+ YourAverageJavaClass that = (YourAverageJavaClass) o;
+ return aByte == that.aByte && aBoolean == that.aBoolean
+ && Double.compare(that.aDouble, aDouble) == 0 && Float.compare(that.aFloat, aFloat) == 0
+ && anInt == that.anInt && transientInt == that.transientInt && aLong == that.aLong
+ && aShort == that.aShort && volatileShort == that.volatileShort
+ && Objects.equals(string, that.string) && Arrays.equals(bytes, that.bytes)
+ && Objects.equals(list, that.list) && Objects.equals(map, that.map);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(aByte, aBoolean, aDouble, aFloat, anInt, transientInt, aLong, aShort,
+ volatileShort, string, list, map);
+ result = 31 * result + Arrays.hashCode(bytes);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "YourAverageJavaClass{"
+ + "aByte=" + aByte + ", aBoolean=" + aBoolean + ", aDouble=" + aDouble + ", aFloat="
+ + aFloat + ", anInt=" + anInt + ", transientInt=" + transientInt + ", aLong=" + aLong
+ + ", aShort=" + aShort + ", volatileShort=" + volatileShort + ", string='" + string + '\''
+ + ", bytes=" + Arrays.toString(bytes) + ", list=" + list + ", map=" + map + '}';
+ }
+
+ @Override
+ public void close() throws IOException {}
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/generated/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/generated/BUILD.bazel
new file mode 100644
index 00000000..fceda64c
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/generated/BUILD.bazel
@@ -0,0 +1,40 @@
+java_binary(
+ name = "NoThrowDoclet",
+ srcs = ["NoThrowDoclet.java"],
+ create_executable = False,
+ tags = ["manual"],
+)
+
+# To regenerate the list of methods, ensure that your local JDK is as recent as possible and contains `lib/src.zip`.
+# This will be the case if you are using the release binaries of the OpenJDK or if the `openjdk-<version>-source`
+# package is installed.
+# Then, execute
+# agent/src/main/java/com/code_intelligence/jazzer/generated/update_java_no_throw_methods_list.sh
+# from the Bazel root and copy the file into
+# org.jacoco.core/src/org/jacoco/core/internal/flow/java_no_throw_methods_list.dat
+# in the CodeIntelligenceTesting/jacoco repository.
+genrule(
+ name = "java_no_throw_methods_list",
+ srcs = [
+ "@local_jdk//:lib/src.zip",
+ ],
+ outs = [
+ "java_no_throw_methods_list.dat.generated",
+ ],
+ cmd = """
+ TMP=$$(mktemp -d) && \
+ unzip $(execpath @local_jdk//:lib/src.zip) -d $$TMP && \
+ $(execpath @local_jdk//:bin/javadoc) \
+ -doclet com.code_intelligence.jazzer.generated.NoThrowDoclet \
+ -docletpath $(execpath :NoThrowDoclet_deploy.jar) \
+ --module java.base \
+ --source-path $$TMP/java.base \
+ --out $@ && \
+ sort -o $@ $@ && \
+ rm -rf $$TMP""",
+ tags = ["manual"],
+ tools = [
+ ":NoThrowDoclet_deploy.jar",
+ "@local_jdk//:bin/javadoc",
+ ],
+)
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/generated/NoThrowDoclet.java b/agent/src/main/java/com/code_intelligence/jazzer/generated/NoThrowDoclet.java
new file mode 100644
index 00000000..1b52a228
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/generated/NoThrowDoclet.java
@@ -0,0 +1,215 @@
+// 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.generated;
+
+import com.sun.source.doctree.DocCommentTree;
+import com.sun.source.doctree.DocTree;
+import com.sun.source.doctree.ThrowsTree;
+import com.sun.source.util.DocTrees;
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.ModuleElement;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.ElementFilter;
+import jdk.javadoc.doclet.Doclet;
+import jdk.javadoc.doclet.DocletEnvironment;
+import jdk.javadoc.doclet.Reporter;
+
+/**
+ * A Doclet that extracts a list of all method signatures in {@code java.*} that are declared not to
+ * throw any exceptions, including {@link RuntimeException} but excluding {@link
+ * VirtualMachineError}.
+ *
+ * Crucially, whereas the throws declaration of a method does not contain subclasses of {@link
+ * RuntimeException}, the {@code @throws} Javadoc tag does.
+ */
+public class NoThrowDoclet implements Doclet {
+ private BufferedWriter out;
+
+ @Override
+ public void init(Locale locale, Reporter reporter) {}
+
+ @Override
+ public String getName() {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public Set<? extends Option> getSupportedOptions() {
+ return Set.of(new Option() {
+ @Override
+ public int getArgumentCount() {
+ return 1;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Output file (.kt)";
+ }
+
+ @Override
+ public Kind getKind() {
+ return Kind.STANDARD;
+ }
+
+ @Override
+ public List<String> getNames() {
+ return List.of("--out");
+ }
+
+ @Override
+ public String getParameters() {
+ return "<output file (.kt)>";
+ }
+
+ @Override
+ public boolean process(String option, List<String> args) {
+ try {
+ out = new BufferedWriter(new FileWriter(args.get(0)));
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return null;
+ }
+
+ private String toDescriptor(TypeMirror type) {
+ switch (type.getKind()) {
+ case BOOLEAN:
+ return "Z";
+ case BYTE:
+ return "B";
+ case CHAR:
+ return "C";
+ case DOUBLE:
+ return "D";
+ case FLOAT:
+ return "F";
+ case INT:
+ return "I";
+ case LONG:
+ return "J";
+ case SHORT:
+ return "S";
+ case VOID:
+ return "V";
+ case ARRAY:
+ return "[" + toDescriptor(((ArrayType) type).getComponentType());
+ case DECLARED:
+ return "L" + getFullyQualifiedName((DeclaredType) type) + ";";
+ case TYPEVAR:
+ return "Ljava/lang/Object;";
+ }
+ throw new IllegalArgumentException(
+ "Unexpected kind " + type.getKind() + ": " + type.toString());
+ }
+
+ private String getFullyQualifiedName(DeclaredType declaredType) {
+ TypeElement element = (TypeElement) declaredType.asElement();
+ return element.getQualifiedName().toString().replace('.', '/');
+ }
+
+ private void handleExecutableElement(DocTrees trees, ExecutableElement executable)
+ throws IOException {
+ if (!executable.getModifiers().contains(Modifier.PUBLIC))
+ return;
+
+ DocCommentTree tree = trees.getDocCommentTree(executable);
+ if (tree != null) {
+ for (DocTree tag : tree.getBlockTags()) {
+ if (tag instanceof ThrowsTree) {
+ return;
+ }
+ }
+ }
+
+ String methodName = executable.getSimpleName().toString();
+ String className =
+ ((TypeElement) executable.getEnclosingElement()).getQualifiedName().toString();
+ String internalClassName = className.replace('.', '/');
+
+ String paramTypeDescriptors = executable.getParameters()
+ .stream()
+ .map(VariableElement::asType)
+ .map(this::toDescriptor)
+ .collect(Collectors.joining(""));
+ String returnTypeDescriptor = toDescriptor(executable.getReturnType());
+ String methodDescriptor = String.format("(%s)%s", paramTypeDescriptors, returnTypeDescriptor);
+
+ out.write(String.format("%s#%s#%s%n", internalClassName, methodName, methodDescriptor));
+ }
+
+ public void handleTypeElement(DocTrees trees, TypeElement typeElement) throws IOException {
+ List<ExecutableElement> executables =
+ ElementFilter.constructorsIn(typeElement.getEnclosedElements());
+ executables.addAll(ElementFilter.methodsIn(typeElement.getEnclosedElements()));
+ for (ExecutableElement executableElement : executables) {
+ handleExecutableElement(trees, executableElement);
+ }
+ }
+
+ @Override
+ public boolean run(DocletEnvironment docletEnvironment) {
+ try {
+ DocTrees trees = docletEnvironment.getDocTrees();
+ for (ModuleElement moduleElement :
+ ElementFilter.modulesIn(docletEnvironment.getSpecifiedElements())) {
+ for (PackageElement packageElement :
+ ElementFilter.packagesIn(moduleElement.getEnclosedElements())) {
+ if (packageElement.getQualifiedName().toString().startsWith("java.")) {
+ for (TypeElement typeElement :
+ ElementFilter.typesIn(packageElement.getEnclosedElements())) {
+ ElementKind kind = typeElement.getKind();
+ if (kind == ElementKind.CLASS || kind == ElementKind.ENUM
+ || kind == ElementKind.INTERFACE) {
+ handleTypeElement(trees, typeElement);
+ }
+ }
+ }
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ try {
+ out.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ return true;
+ }
+} \ No newline at end of file
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/generated/update_java_no_throw_methods_list.sh b/agent/src/main/java/com/code_intelligence/jazzer/generated/update_java_no_throw_methods_list.sh
new file mode 100755
index 00000000..1463c602
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/generated/update_java_no_throw_methods_list.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env sh
+# 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.
+
+set -e
+bazel build //agent/src/main/java/com/code_intelligence/jazzer/generated:java_no_throw_methods_list
+cp bazel-bin/agent/src/main/java/com/code_intelligence/jazzer/generated/java_no_throw_methods_list.dat.generated agent/src/main/java/com/code_intelligence/jazzer/generated/java_no_throw_methods_list.dat
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel
new file mode 100644
index 00000000..50d10705
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel
@@ -0,0 +1,45 @@
+load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library")
+load("@com_github_johnynek_bazel_jar_jar//:jar_jar.bzl", "jar_jar")
+
+kt_jvm_library(
+ name = "instrumentor",
+ srcs = [
+ "ClassInstrumentor.kt",
+ "CoverageRecorder.kt",
+ "DescriptorUtils.kt",
+ "DeterministicRandom.kt",
+ "EdgeCoverageInstrumentor.kt",
+ "Hook.kt",
+ "HookInstrumentor.kt",
+ "HookMethodVisitor.kt",
+ "Instrumentor.kt",
+ "TraceDataFlowInstrumentor.kt",
+ ],
+ visibility = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/agent:__pkg__",
+ "//agent/src/test/java/com/code_intelligence/jazzer/instrumentor:__pkg__",
+ ],
+ deps = [
+ ":shaded_deps",
+ "//agent/src/main/java/com/code_intelligence/jazzer/runtime",
+ "//agent/src/main/java/com/code_intelligence/jazzer/utils",
+ "@com_github_classgraph_classgraph//:classgraph",
+ "@com_github_jetbrains_kotlin//:kotlin-reflect",
+ ],
+)
+
+jar_jar(
+ name = "shaded_deps",
+ input_jar = "unshaded_deps_deploy.jar",
+ rules = "shade_rules",
+)
+
+java_binary(
+ name = "unshaded_deps",
+ create_executable = False,
+ runtime_deps = [
+ "@jazzer_jacoco//:jacoco_internal",
+ "@jazzer_ow2_asm//:asm",
+ "@jazzer_ow2_asm//:asm_commons",
+ ],
+)
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt
new file mode 100644
index 00000000..f6728a1a
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt
@@ -0,0 +1,53 @@
+// 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.instrumentor
+
+fun extractClassFileMajorVersion(classfileBuffer: ByteArray): Int {
+ return ((classfileBuffer[6].toInt() and 0xff) shl 8) or (classfileBuffer[7].toInt() and 0xff)
+}
+
+class ClassInstrumentor constructor(bytecode: ByteArray) {
+
+ var instrumentedBytecode = bytecode
+ private set
+
+ fun coverage(initialEdgeId: Int): Int {
+ val edgeCoverageInstrumentor = EdgeCoverageInstrumentor(initialEdgeId)
+ instrumentedBytecode = edgeCoverageInstrumentor.instrument(instrumentedBytecode)
+ return edgeCoverageInstrumentor.numEdges
+ }
+
+ fun traceDataFlow(instrumentations: Set<InstrumentationType>) {
+ instrumentedBytecode = TraceDataFlowInstrumentor(instrumentations).instrument(instrumentedBytecode)
+ }
+
+ fun hooks(hooks: Iterable<Hook>) {
+ instrumentedBytecode = HookInstrumentor(
+ hooks,
+ java6Mode = extractClassFileMajorVersion(instrumentedBytecode) < 51
+ ).instrument(instrumentedBytecode)
+ }
+
+ companion object {
+ init {
+ try {
+ // Calls JNI_OnLoad_jazzer_initialize in the driver, which registers the native methods.
+ System.loadLibrary("jazzer_initialize")
+ } catch (_: UnsatisfiedLinkError) {
+ // Make it possible to use (parts of) the agent without the driver.
+ }
+ }
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt
new file mode 100644
index 00000000..65956189
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt
@@ -0,0 +1,229 @@
+// 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.instrumentor
+
+import com.code_intelligence.jazzer.runtime.CoverageMap
+import com.code_intelligence.jazzer.third_party.jacoco.core.analysis.CoverageBuilder
+import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionData
+import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionDataReader
+import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionDataStore
+import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionDataWriter
+import com.code_intelligence.jazzer.third_party.jacoco.core.data.SessionInfo
+import com.code_intelligence.jazzer.third_party.jacoco.core.data.SessionInfoStore
+import com.code_intelligence.jazzer.third_party.jacoco.core.internal.data.CRC64
+import com.code_intelligence.jazzer.utils.ClassNameGlobber
+import io.github.classgraph.ClassGraph
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.time.Instant
+import java.util.UUID
+
+private data class InstrumentedClassInfo(
+ val classId: Long,
+ val initialEdgeId: Int,
+ val nextEdgeId: Int,
+ val bytecode: ByteArray,
+)
+
+object CoverageRecorder {
+ var classNameGlobber = ClassNameGlobber(emptyList(), emptyList())
+ private val instrumentedClassInfo = mutableMapOf<String, InstrumentedClassInfo>()
+ private var startTimestamp: Instant? = null
+ private val additionalCoverage = mutableSetOf<Int>()
+
+ fun recordInstrumentedClass(internalClassName: String, bytecode: ByteArray, firstId: Int, numIds: Int) {
+ if (startTimestamp == null)
+ startTimestamp = Instant.now()
+ instrumentedClassInfo[internalClassName] = InstrumentedClassInfo(
+ CRC64.classId(bytecode), firstId, firstId + numIds, bytecode
+ )
+ }
+
+ /**
+ * Manually records coverage IDs based on the current state of [CoverageMap.mem].
+ * Should be called after static initializers have run.
+ */
+ @JvmStatic
+ fun updateCoveredIdsWithCoverageMap() {
+ val mem = CoverageMap.mem
+ val size = mem.capacity()
+ additionalCoverage.addAll((0 until size).filter { mem[it] > 0 })
+ }
+
+ @JvmStatic
+ fun replayCoveredIds() {
+ val mem = CoverageMap.mem
+ for (coverageId in additionalCoverage) {
+ mem.put(coverageId, 1)
+ }
+ }
+
+ @JvmStatic
+ fun computeFileCoverage(coveredIds: IntArray): String {
+ val coverage = analyzeCoverage(coveredIds.toSet()) ?: return "No classes were instrumented"
+ return coverage.sourceFiles.joinToString(
+ "\n",
+ prefix = "Branch coverage:\n",
+ postfix = "\n\n"
+ ) { fileCoverage ->
+ val counter = fileCoverage.branchCounter
+ val percentage = 100 * counter.coveredRatio
+ "${fileCoverage.name}: ${counter.coveredCount}/${counter.totalCount} (${percentage.format(2)}%)"
+ } + coverage.sourceFiles.joinToString(
+ "\n",
+ prefix = "Line coverage:\n",
+ postfix = "\n\n"
+ ) { fileCoverage ->
+ val counter = fileCoverage.lineCounter
+ val percentage = 100 * counter.coveredRatio
+ "${fileCoverage.name}: ${counter.coveredCount}/${counter.totalCount} (${percentage.format(2)}%)"
+ } + coverage.sourceFiles.joinToString(
+ "\n",
+ prefix = "Incompletely covered lines:\n",
+ postfix = "\n\n"
+ ) { fileCoverage ->
+ "${fileCoverage.name}: " + (fileCoverage.firstLine..fileCoverage.lastLine).filter {
+ val instructions = fileCoverage.getLine(it).instructionCounter
+ instructions.coveredCount in 1 until instructions.totalCount
+ }.toString()
+ } + coverage.sourceFiles.joinToString(
+ "\n",
+ prefix = "Missed lines:\n",
+ ) { fileCoverage ->
+ "${fileCoverage.name}: " + (fileCoverage.firstLine..fileCoverage.lastLine).filter {
+ val instructions = fileCoverage.getLine(it).instructionCounter
+ instructions.coveredCount == 0 && instructions.totalCount > 0
+ }.toString()
+ }
+ }
+
+ private fun Double.format(digits: Int) = "%.${digits}f".format(this)
+
+ fun dumpJacocoCoverage(coveredIds: Set<Int>): ByteArray? {
+ // Update the list of covered IDs with the coverage information for the current run.
+ updateCoveredIdsWithCoverageMap()
+
+ val dumpTimestamp = Instant.now()
+ val outStream = ByteArrayOutputStream()
+ val outWriter = ExecutionDataWriter(outStream)
+ // Return null if no class has been instrumented.
+ val startTimestamp = startTimestamp ?: return null
+ outWriter.visitSessionInfo(
+ SessionInfo(UUID.randomUUID().toString(), startTimestamp.epochSecond, dumpTimestamp.epochSecond)
+ )
+
+ val sortedCoveredIds = (additionalCoverage + coveredIds).sorted().toIntArray()
+ for ((internalClassName, info) in instrumentedClassInfo) {
+ // Determine the subarray of coverage IDs in sortedCoveredIds that contains the IDs generated while
+ // instrumenting the current class. Since the ID array is sorted, use binary search.
+ var coveredIdsStart = sortedCoveredIds.binarySearch(info.initialEdgeId)
+ if (coveredIdsStart < 0) {
+ coveredIdsStart = -(coveredIdsStart + 1)
+ }
+ var coveredIdsEnd = sortedCoveredIds.binarySearch(info.nextEdgeId)
+ if (coveredIdsEnd < 0) {
+ coveredIdsEnd = -(coveredIdsEnd + 1)
+ }
+ if (coveredIdsStart == coveredIdsEnd) {
+ // No coverage data for the class.
+ continue
+ }
+ check(coveredIdsStart in 0 until coveredIdsEnd && coveredIdsEnd <= sortedCoveredIds.size) {
+ "Invalid range [$coveredIdsStart, $coveredIdsEnd) with coveredIds.size=${sortedCoveredIds.size}"
+ }
+ // Generate a probes array for the current class only, i.e., mapping info.initialEdgeId to 0.
+ val probes = BooleanArray(info.nextEdgeId - info.initialEdgeId)
+ (coveredIdsStart until coveredIdsEnd).asSequence()
+ .map {
+ val globalEdgeId = sortedCoveredIds[it]
+ globalEdgeId - info.initialEdgeId
+ }
+ .forEach { classLocalEdgeId ->
+ probes[classLocalEdgeId] = true
+ }
+ outWriter.visitClassExecution(ExecutionData(info.classId, internalClassName, probes))
+ }
+ return outStream.toByteArray()
+ }
+
+ fun analyzeCoverage(coveredIds: Set<Int>): CoverageBuilder? {
+ return try {
+ val coverage = CoverageBuilder()
+ analyzeAllUncoveredClasses(coverage)
+ val rawExecutionData = dumpJacocoCoverage(coveredIds) ?: return null
+ val executionDataStore = ExecutionDataStore()
+ val sessionInfoStore = SessionInfoStore()
+ ByteArrayInputStream(rawExecutionData).use { stream ->
+ ExecutionDataReader(stream).run {
+ setExecutionDataVisitor(executionDataStore)
+ setSessionInfoVisitor(sessionInfoStore)
+ read()
+ }
+ }
+ for ((internalClassName, info) in instrumentedClassInfo) {
+ EdgeCoverageInstrumentor(0).analyze(
+ executionDataStore,
+ coverage,
+ info.bytecode,
+ internalClassName
+ )
+ }
+ coverage
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+
+ /**
+ * Traverses the entire classpath and analyzes all uncovered classes that match the include/exclude pattern.
+ * The returned [CoverageBuilder] will report coverage information for *all* classes on the classpath, not just
+ * those that were loaded while the fuzzer ran.
+ */
+ private fun analyzeAllUncoveredClasses(coverage: CoverageBuilder): CoverageBuilder {
+ val coveredClassNames = instrumentedClassInfo
+ .keys
+ .asSequence()
+ .map { it.replace('/', '.') }
+ .toSet()
+ val emptyExecutionDataStore = ExecutionDataStore()
+ ClassGraph()
+ .enableClassInfo()
+ .ignoreClassVisibility()
+ .rejectPackages(
+ // Always exclude Jazzer-internal packages (including ClassGraph itself) from coverage reports. Classes
+ // from the Java standard library are never traversed.
+ "com.code_intelligence.jazzer.*",
+ "jaz",
+ )
+ .scan().use { result ->
+ result.allClasses
+ .asSequence()
+ .filter { classInfo -> classNameGlobber.includes(classInfo.name) }
+ .filterNot { classInfo -> classInfo.name in coveredClassNames }
+ .forEach { classInfo ->
+ classInfo.resource.use { resource ->
+ EdgeCoverageInstrumentor(0).analyze(
+ emptyExecutionDataStore,
+ coverage,
+ resource.load(),
+ classInfo.name.replace('.', '/')
+ )
+ }
+ }
+ }
+ return coverage
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt
new file mode 100644
index 00000000..80cbe80b
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt
@@ -0,0 +1,90 @@
+// 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.instrumentor
+
+internal fun isPrimitiveType(typeDescriptor: String): Boolean {
+ return typeDescriptor in arrayOf("B", "C", "D", "F", "I", "J", "S", "V", "Z")
+}
+
+private fun isPrimitiveType(typeDescriptor: Char) = isPrimitiveType(typeDescriptor.toString())
+
+internal fun getWrapperTypeDescriptor(typeDescriptor: String): String = when (typeDescriptor) {
+ "B" -> "Ljava/lang/Byte;"
+ "C" -> "Ljava/lang/Character;"
+ "D" -> "Ljava/lang/Double;"
+ "F" -> "Ljava/lang/Float;"
+ "I" -> "Ljava/lang/Integer;"
+ "J" -> "Ljava/lang/Long;"
+ "S" -> "Ljava/lang/Short;"
+ "V" -> "Ljava/lang/Void;"
+ "Z" -> "Ljava/lang/Boolean;"
+ else -> typeDescriptor
+}
+
+// Removes the 'L' and ';' prefix/suffix from signatures to get the full class name.
+// Note that array signatures '[Ljava/lang/String;' already have the correct form.
+internal fun extractInternalClassName(typeDescriptor: String): String {
+ return if (typeDescriptor.startsWith("L") && typeDescriptor.endsWith(";")) {
+ typeDescriptor.substring(1, typeDescriptor.length - 1)
+ } else {
+ typeDescriptor
+ }
+}
+
+internal fun extractParameterTypeDescriptors(methodDescriptor: String): List<String> {
+ require(methodDescriptor.startsWith('(')) { "Method descriptor must start with '('" }
+ val endOfParameterPart = methodDescriptor.indexOf(')') - 1
+ require(endOfParameterPart >= 0) { "Method descriptor must contain ')'" }
+ var remainingDescriptorList = methodDescriptor.substring(1..endOfParameterPart)
+ val parameterDescriptors = mutableListOf<String>()
+ while (remainingDescriptorList.isNotEmpty()) {
+ val nextDescriptor = extractNextTypeDescriptor(remainingDescriptorList)
+ parameterDescriptors.add(nextDescriptor)
+ remainingDescriptorList = remainingDescriptorList.removePrefix(nextDescriptor)
+ }
+ return parameterDescriptors
+}
+
+internal fun extractReturnTypeDescriptor(methodDescriptor: String): String {
+ require(methodDescriptor.startsWith('(')) { "Method descriptor must start with '('" }
+ val endBracketPos = methodDescriptor.indexOf(')')
+ require(endBracketPos >= 0) { "Method descriptor must contain ')'" }
+ val startOfReturnValue = endBracketPos + 1
+ return extractNextTypeDescriptor(methodDescriptor.substring(startOfReturnValue))
+}
+
+private fun extractNextTypeDescriptor(input: String): String {
+ require(input.isNotEmpty()) { "Type descriptor must not be empty" }
+ // Skip over arbitrarily many '[' to support multi-dimensional arrays.
+ val firstNonArrayPrefixCharPos = input.indexOfFirst { it != '[' }
+ require(firstNonArrayPrefixCharPos >= 0) { "Array descriptor must contain type" }
+ val firstTypeChar = input[firstNonArrayPrefixCharPos]
+ return when {
+ // Primitive type
+ isPrimitiveType(firstTypeChar) -> {
+ input.substring(0..firstNonArrayPrefixCharPos)
+ }
+ // Object type
+ firstTypeChar == 'L' -> {
+ val endOfClassNamePos = input.indexOf(';')
+ require(endOfClassNamePos > 0) { "Class type indicated by L must end with ;" }
+ input.substring(0..endOfClassNamePos)
+ }
+ // Invalid type
+ else -> {
+ throw IllegalArgumentException("Invalid type: $firstTypeChar")
+ }
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt
new file mode 100644
index 00000000..d4210dc4
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt
@@ -0,0 +1,35 @@
+// 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.instrumentor
+
+import java.security.MessageDigest
+import java.security.SecureRandom
+
+// This RNG is resistant to collisions (even under XOR) but fully deterministic.
+internal class DeterministicRandom(vararg contexts: String) {
+ private val random = SecureRandom.getInstance("SHA1PRNG").apply {
+ val contextHash = MessageDigest.getInstance("SHA-256").run {
+ for (context in contexts) {
+ update(context.toByteArray())
+ }
+ digest()
+ }
+ setSeed(contextHash)
+ }
+
+ fun nextInt(bound: Int) = random.nextInt(bound)
+
+ fun nextInt() = random.nextInt()
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt
new file mode 100644
index 00000000..ba5b7ee9
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt
@@ -0,0 +1,201 @@
+// 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.instrumentor
+
+import com.code_intelligence.jazzer.runtime.CoverageMap
+import com.code_intelligence.jazzer.third_party.jacoco.core.analysis.Analyzer
+import com.code_intelligence.jazzer.third_party.jacoco.core.analysis.ICoverageVisitor
+import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionDataStore
+import com.code_intelligence.jazzer.third_party.jacoco.core.internal.flow.ClassProbesAdapter
+import com.code_intelligence.jazzer.third_party.jacoco.core.internal.flow.ClassProbesVisitor
+import com.code_intelligence.jazzer.third_party.jacoco.core.internal.flow.IClassProbesAdapterFactory
+import com.code_intelligence.jazzer.third_party.jacoco.core.internal.flow.JavaNoThrowMethods
+import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.ClassInstrumenter
+import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.IProbeArrayStrategy
+import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.IProbeInserterFactory
+import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.InstrSupport
+import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.ProbeInserter
+import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassReader
+import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassVisitor
+import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassWriter
+import com.code_intelligence.jazzer.third_party.objectweb.asm.MethodVisitor
+import com.code_intelligence.jazzer.third_party.objectweb.asm.Opcodes
+import kotlin.math.max
+
+class EdgeCoverageInstrumentor(
+ private val initialEdgeId: Int,
+ private val coverageMapClass: Class<*> = CoverageMap::class.java
+) : Instrumentor {
+ private var nextEdgeId = initialEdgeId
+ private val coverageMapInternalClassName = coverageMapClass.name.replace('.', '/')
+ init {
+ if (isTesting) {
+ JavaNoThrowMethods.isTesting = true
+ }
+ }
+
+ override fun instrument(bytecode: ByteArray): ByteArray {
+ val reader = InstrSupport.classReaderFor(bytecode)
+ val writer = ClassWriter(reader, 0)
+ val version = InstrSupport.getMajorVersion(reader)
+ val visitor = EdgeCoverageClassProbesAdapter(
+ ClassInstrumenter(edgeCoverageProbeArrayStrategy, edgeCoverageProbeInserterFactory, writer),
+ InstrSupport.needsFrames(version)
+ )
+ reader.accept(visitor, ClassReader.EXPAND_FRAMES)
+ return writer.toByteArray()
+ }
+
+ fun analyze(executionData: ExecutionDataStore, coverageVisitor: ICoverageVisitor, bytecode: ByteArray, internalClassName: String) {
+ Analyzer(executionData, coverageVisitor, edgeCoverageClassProbesAdapterFactory).run {
+ analyzeClass(bytecode, internalClassName)
+ }
+ }
+
+ val numEdges
+ get() = nextEdgeId - initialEdgeId
+
+ private val isTesting
+ get() = coverageMapClass != CoverageMap::class.java
+
+ private fun nextEdgeId(): Int {
+ if (nextEdgeId >= CoverageMap.mem.capacity()) {
+ if (!isTesting) {
+ CoverageMap.enlargeCoverageMap()
+ }
+ }
+ return nextEdgeId++
+ }
+
+ /**
+ * The maximal number of stack elements used by [loadCoverageMap].
+ */
+ private val loadCoverageMapStackSize = 1
+
+ /**
+ * Inject bytecode that loads the coverage map into local variable [variable].
+ */
+ private fun loadCoverageMap(mv: MethodVisitor, variable: Int) {
+ mv.apply {
+ visitFieldInsn(
+ Opcodes.GETSTATIC,
+ coverageMapInternalClassName,
+ "mem",
+ "Ljava/nio/ByteBuffer;"
+ )
+ // Stack: mem (maxStack: 1)
+ visitVarInsn(Opcodes.ASTORE, variable)
+ }
+ }
+
+ /**
+ * The maximal number of stack elements used by [instrumentControlFlowEdge].
+ */
+ private val instrumentControlFlowEdgeStackSize = 5
+
+ /**
+ * Inject bytecode instrumentation on a control flow edge with ID [edgeId]. The coverage map can be loaded from
+ * local variable [variable].
+ */
+ private fun instrumentControlFlowEdge(mv: MethodVisitor, edgeId: Int, variable: Int) {
+ mv.apply {
+ visitVarInsn(Opcodes.ALOAD, variable)
+ // Stack: mem
+ push(edgeId)
+ // Stack: mem | edgeId
+ visitInsn(Opcodes.DUP2)
+ // Stack: mem | edgeId | mem | edgeId
+ visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/nio/ByteBuffer", "get", "(I)B", false)
+ // Increment the counter, but ensure that it never stays at 0 after an overflow by incrementing it again in
+ // that case.
+ // This approach performs better than saturating the counter at 255 (see Section 3.3 of
+ // https://www.usenix.org/system/files/woot20-paper-fioraldi.pdf)
+ // Stack: mem | edgeId | counter (sign-extended to int)
+ push(0xff)
+ // Stack: mem | edgeId | counter (sign-extended to int) | 0x000000ff
+ visitInsn(Opcodes.IAND)
+ // Stack: mem | edgeId | counter (zero-extended to int)
+ push(1)
+ // Stack: mem | edgeId | counter | 1
+ visitInsn(Opcodes.IADD)
+ // Stack: mem | edgeId | counter + 1
+ visitInsn(Opcodes.DUP)
+ // Stack: mem | edgeId | counter + 1 | counter + 1
+ push(8)
+ // Stack: mem | edgeId | counter + 1 | counter + 1 | 8 (maxStack: +5)
+ visitInsn(Opcodes.ISHR)
+ // Stack: mem | edgeId | counter + 1 | 1 if the increment overflowed to 0, 0 otherwise
+ visitInsn(Opcodes.IADD)
+ // Stack: mem | edgeId | counter + 2 if the increment overflowed, counter + 1 otherwise
+ visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/nio/ByteBuffer", "put", "(IB)Ljava/nio/ByteBuffer;", false)
+ // Stack: mem
+ visitInsn(Opcodes.POP)
+ if (isTesting) {
+ visitMethodInsn(Opcodes.INVOKESTATIC, coverageMapInternalClassName, "updated", "()V", false)
+ }
+ }
+ }
+
+// The remainder of this file interfaces with classes in org.jacoco.core.internal. Changes to this part should not be
+// necessary unless JaCoCo is updated or the way we instrument for coverage changes fundamentally.
+
+ /**
+ * A [ProbeInserter] that injects the bytecode instrumentation returned by [instrumentControlFlowEdge] and modifies
+ * the stack size and number of local variables accordingly.
+ */
+ private inner class EdgeCoverageProbeInserter(
+ access: Int,
+ name: String,
+ desc: String,
+ mv: MethodVisitor,
+ arrayStrategy: IProbeArrayStrategy,
+ ) : ProbeInserter(access, name, desc, mv, arrayStrategy) {
+ override fun insertProbe(id: Int) {
+ instrumentControlFlowEdge(mv, id, variable)
+ }
+
+ override fun visitMaxs(maxStack: Int, maxLocals: Int) {
+ val newMaxStack = max(maxStack + instrumentControlFlowEdgeStackSize, loadCoverageMapStackSize)
+ mv.visitMaxs(newMaxStack, maxLocals + 1)
+ }
+ }
+
+ private val edgeCoverageProbeInserterFactory =
+ IProbeInserterFactory { access, name, desc, mv, arrayStrategy ->
+ EdgeCoverageProbeInserter(access, name, desc, mv, arrayStrategy)
+ }
+
+ private inner class EdgeCoverageClassProbesAdapter(cv: ClassProbesVisitor, trackFrames: Boolean) :
+ ClassProbesAdapter(cv, trackFrames) {
+ override fun nextId(): Int = nextEdgeId()
+ }
+
+ private val edgeCoverageClassProbesAdapterFactory = IClassProbesAdapterFactory { probesVisitor, trackFrames ->
+ EdgeCoverageClassProbesAdapter(probesVisitor, trackFrames)
+ }
+
+ private val edgeCoverageProbeArrayStrategy = object : IProbeArrayStrategy {
+ override fun storeInstance(mv: MethodVisitor, clinit: Boolean, variable: Int): Int {
+ loadCoverageMap(mv, variable)
+ return loadCoverageMapStackSize
+ }
+
+ override fun addMembers(cv: ClassVisitor, probeCount: Int) {}
+ }
+
+ private fun MethodVisitor.push(value: Int) {
+ InstrSupport.push(this, value)
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt
new file mode 100644
index 00000000..92106e14
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt
@@ -0,0 +1,119 @@
+// 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:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+
+package com.code_intelligence.jazzer.instrumentor
+
+import com.code_intelligence.jazzer.api.HookType
+import com.code_intelligence.jazzer.api.MethodHook
+import com.code_intelligence.jazzer.api.MethodHooks
+import com.code_intelligence.jazzer.utils.descriptor
+import java.lang.invoke.MethodHandle
+import java.lang.reflect.Method
+import java.lang.reflect.Modifier
+
+class Hook private constructor(hookMethod: Method, annotation: MethodHook) {
+ // Allowing arbitrary exterior whitespace in the target class name allows for an easy workaround
+ // for mangled hooks due to shading applied to hooks.
+ private val targetClassName = annotation.targetClassName.trim()
+ val targetMethodName = annotation.targetMethod
+ val targetMethodDescriptor = annotation.targetMethodDescriptor.takeIf { it.isNotEmpty() }
+ val hookType = annotation.type
+
+ val targetInternalClassName = targetClassName.replace('.', '/')
+ private val targetReturnTypeDescriptor = targetMethodDescriptor?.let { extractReturnTypeDescriptor(it) }
+ private val targetWrappedReturnTypeDescriptor = targetReturnTypeDescriptor?.let { getWrapperTypeDescriptor(it) }
+
+ private val hookClassName: String = hookMethod.declaringClass.name
+ val hookInternalClassName = hookClassName.replace('.', '/')
+ val hookMethodName: String = hookMethod.name
+ val hookMethodDescriptor = hookMethod.descriptor
+
+ override fun toString(): String {
+ return "$hookType $targetClassName.$targetMethodName: $hookClassName.$hookMethodName"
+ }
+
+ companion object {
+
+ fun verifyAndGetHook(hookMethod: Method, hookData: MethodHook): Hook {
+ // Verify the annotation type and extract information for debug statements.
+ val potentialHook = Hook(hookMethod, hookData)
+
+ // Verify the hook method's modifiers (public static).
+ require(Modifier.isPublic(hookMethod.modifiers)) { "$potentialHook: hook method must be public" }
+ require(Modifier.isStatic(hookMethod.modifiers)) { "$potentialHook: hook method must be static" }
+
+ // Verify the hook method's parameter count.
+ val numParameters = hookMethod.parameters.size
+ when (hookData.type) {
+ HookType.BEFORE, HookType.REPLACE -> require(numParameters == 4) { "$potentialHook: incorrect number of parameters (expected 4)" }
+ HookType.AFTER -> require(numParameters == 5) { "$potentialHook: incorrect number of parameters (expected 5)" }
+ }
+
+ // Verify the hook method's parameter types.
+ val parameterTypes = hookMethod.parameterTypes
+ require(parameterTypes[0] == MethodHandle::class.java) { "$potentialHook: first parameter must have type MethodHandle" }
+ require(parameterTypes[1] == Object::class.java || parameterTypes[1].name == potentialHook.targetClassName) { "$potentialHook: second parameter must have type Object or ${potentialHook.targetClassName}" }
+ require(parameterTypes[2] == Array<Object>::class.java) { "$potentialHook: third parameter must have type Object[]" }
+ require(parameterTypes[3] == Int::class.javaPrimitiveType) { "$potentialHook: fourth parameter must have type int" }
+
+ // Verify the hook method's return type if possible.
+ when (hookData.type) {
+ HookType.BEFORE, HookType.AFTER -> require(hookMethod.returnType == Void.TYPE) {
+ "$potentialHook: return type must be void"
+ }
+ HookType.REPLACE -> if (potentialHook.targetReturnTypeDescriptor != null) {
+ val returnTypeDescriptor = hookMethod.returnType.descriptor
+ if (potentialHook.targetReturnTypeDescriptor == "V") {
+ require(returnTypeDescriptor == "V") { "$potentialHook: return type must be void to match targetMethodDescriptor" }
+ } else {
+ require(
+ returnTypeDescriptor in listOf(
+ java.lang.Object::class.java.descriptor,
+ potentialHook.targetReturnTypeDescriptor,
+ potentialHook.targetWrappedReturnTypeDescriptor
+ )
+ ) {
+ "$potentialHook: return type must have type Object or match the descriptors ${potentialHook.targetReturnTypeDescriptor} or ${potentialHook.targetWrappedReturnTypeDescriptor}"
+ }
+ }
+ }
+ }
+
+ // AfterMethodHook only: Verify the type of the last parameter if known.
+ if (hookData.type == HookType.AFTER && potentialHook.targetReturnTypeDescriptor != null) {
+ require(
+ parameterTypes[4] == java.lang.Object::class.java ||
+ parameterTypes[4].descriptor == potentialHook.targetWrappedReturnTypeDescriptor
+ ) {
+ "$potentialHook: fifth parameter must have type Object or match the descriptor ${potentialHook.targetWrappedReturnTypeDescriptor}"
+ }
+ }
+
+ return potentialHook
+ }
+ }
+}
+
+fun loadHooks(hookClass: Class<*>): List<Hook> {
+ val hooks = mutableListOf<Hook>()
+ for (method in hookClass.methods) {
+ method.getAnnotation(MethodHook::class.java)?.let { hooks.add(Hook.verifyAndGetHook(method, it)) }
+ method.getAnnotation(MethodHooks::class.java)?.let {
+ it.value.forEach { hookAnnotation -> hooks.add(Hook.verifyAndGetHook(method, hookAnnotation)) }
+ }
+ }
+ return hooks
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt
new file mode 100644
index 00000000..ac5f1780
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt
@@ -0,0 +1,48 @@
+// 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.instrumentor
+
+import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassReader
+import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassVisitor
+import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassWriter
+import com.code_intelligence.jazzer.third_party.objectweb.asm.MethodVisitor
+
+internal class HookInstrumentor(private val hooks: Iterable<Hook>, private val java6Mode: Boolean) : Instrumentor {
+
+ private lateinit var random: DeterministicRandom
+
+ override fun instrument(bytecode: ByteArray): ByteArray {
+ val reader = ClassReader(bytecode)
+ val writer = ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
+ random = DeterministicRandom("hook", reader.className)
+ val interceptor = object : ClassVisitor(Instrumentor.ASM_API_VERSION, writer) {
+ override fun visitMethod(
+ access: Int,
+ name: String?,
+ descriptor: String?,
+ signature: String?,
+ exceptions: Array<String>?,
+ ): MethodVisitor? {
+ val mv = cv.visitMethod(access, name, descriptor, signature, exceptions) ?: return null
+ return if (shouldInstrument(access))
+ makeHookMethodVisitor(access, descriptor, mv, hooks, java6Mode, random)
+ else
+ mv
+ }
+ }
+ reader.accept(interceptor, ClassReader.EXPAND_FRAMES)
+ return writer.toByteArray()
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt
new file mode 100644
index 00000000..7c23c703
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt
@@ -0,0 +1,386 @@
+// 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.instrumentor
+
+import com.code_intelligence.jazzer.api.HookType
+import com.code_intelligence.jazzer.third_party.objectweb.asm.Handle
+import com.code_intelligence.jazzer.third_party.objectweb.asm.MethodVisitor
+import com.code_intelligence.jazzer.third_party.objectweb.asm.Opcodes
+import com.code_intelligence.jazzer.third_party.objectweb.asm.Type
+import com.code_intelligence.jazzer.third_party.objectweb.asm.commons.LocalVariablesSorter
+
+internal fun makeHookMethodVisitor(
+ access: Int,
+ descriptor: String?,
+ methodVisitor: MethodVisitor?,
+ hooks: Iterable<Hook>,
+ java6Mode: Boolean,
+ random: DeterministicRandom,
+): MethodVisitor {
+ return HookMethodVisitor(access, descriptor, methodVisitor, hooks, java6Mode, random).lvs
+}
+
+private class HookMethodVisitor(
+ access: Int,
+ descriptor: String?,
+ methodVisitor: MethodVisitor?,
+ hooks: Iterable<Hook>,
+ private val java6Mode: Boolean,
+ private val random: DeterministicRandom,
+) : MethodVisitor(Instrumentor.ASM_API_VERSION, methodVisitor) {
+
+ val lvs = object : LocalVariablesSorter(Instrumentor.ASM_API_VERSION, access, descriptor, this) {
+ override fun updateNewLocals(newLocals: Array<Any>) {
+ // The local variables involved in calling hooks do not need to outlive the current
+ // basic block and should thus not appear in stack map frames. By requesting the
+ // LocalVariableSorter to fill their entries in stack map frames with TOP, they will
+ // be treated like an unused local variable slot.
+ newLocals.fill(Opcodes.TOP)
+ }
+ }
+
+ private val hooks = hooks.associateBy { hook ->
+ var hookKey = "${hook.hookType}#${hook.targetInternalClassName}#${hook.targetMethodName}"
+ if (hook.targetMethodDescriptor != null)
+ hookKey += "#${hook.targetMethodDescriptor}"
+ hookKey
+ }
+
+ override fun visitMethodInsn(
+ opcode: Int,
+ owner: String,
+ methodName: String,
+ methodDescriptor: String,
+ isInterface: Boolean,
+ ) {
+ if (!isMethodInvocationOp(opcode)) {
+ mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
+ return
+ }
+ handleMethodInsn(HookType.BEFORE, opcode, owner, methodName, methodDescriptor, isInterface)
+ }
+
+ /**
+ * Emits the bytecode for a method call instruction for the next applicable hook type in order (BEFORE, REPLACE,
+ * AFTER). Since the instrumented code is indistinguishable from an uninstrumented call instruction, it can be
+ * safely nested. Combining REPLACE hooks with other hooks is however not supported as these hooks already subsume
+ * the functionality of BEFORE and AFTER hooks.
+ */
+ private fun visitNextHookTypeOrCall(
+ hookType: HookType,
+ appliedHook: Boolean,
+ opcode: Int,
+ owner: String,
+ methodName: String,
+ methodDescriptor: String,
+ isInterface: Boolean,
+ ) = when (hookType) {
+ HookType.BEFORE -> {
+ val nextHookType = if (appliedHook) {
+ // After a BEFORE hook has been applied, we can safely apply an AFTER hook by replacing the actual
+ // call instruction with the full bytecode injected for the AFTER hook.
+ HookType.AFTER
+ } else {
+ // If no BEFORE hook is registered, look for a REPLACE hook next.
+ HookType.REPLACE
+ }
+ handleMethodInsn(nextHookType, opcode, owner, methodName, methodDescriptor, isInterface)
+ }
+ HookType.REPLACE -> {
+ // REPLACE hooks can't (and don't need to) be mixed with other hooks. We only cycle through them if we
+ // couldn't find a matching REPLACE hook, in which case we try an AFTER hook next.
+ require(!appliedHook)
+ handleMethodInsn(HookType.AFTER, opcode, owner, methodName, methodDescriptor, isInterface)
+ }
+ // An AFTER hook is always the last in the chain. Whether a hook has been applied or not, always emit the
+ // actual call instruction.
+ HookType.AFTER -> mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
+ }
+
+ fun handleMethodInsn(
+ hookType: HookType,
+ opcode: Int,
+ owner: String,
+ methodName: String,
+ methodDescriptor: String,
+ isInterface: Boolean,
+ ) {
+ val hook = findMatchingHook(hookType, owner, methodName, methodDescriptor)
+ if (hook == null) {
+ visitNextHookTypeOrCall(hookType, false, opcode, owner, methodName, methodDescriptor, isInterface)
+ return
+ }
+
+ // The hookId is used to identify a call site.
+ val hookId = random.nextInt()
+
+ val paramDescriptors = extractParameterTypeDescriptors(methodDescriptor)
+ val localObjArr = storeMethodArguments(paramDescriptors)
+ // If the method we're hooking is not static there is now a reference to
+ // the object the method was invoked on at the top of the stack.
+ // If the method is static, that object is missing. We make up for it by pushing a null ref.
+ if (opcode == Opcodes.INVOKESTATIC) {
+ mv.visitInsn(Opcodes.ACONST_NULL)
+ }
+
+ // Save the owner object to a new local variable
+ val ownerDescriptor = "L$owner;"
+ val localOwnerObj = lvs.newLocal(Type.getType(ownerDescriptor))
+ mv.visitVarInsn(Opcodes.ASTORE, localOwnerObj) // consume objectref
+ // We now removed all values for the original method call from the operand stack
+ // and saved them to local variables.
+
+ // Start to build the arguments for the hook method.
+ if (methodName == "<init>") {
+ // Special case for constructors:
+ // We cannot create a MethodHandle for a constructor, so we push null instead.
+ mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
+ // Only pass the this object if it has been initialized by the time the hook is invoked.
+ if (hook.hookType == HookType.AFTER) {
+ mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj)
+ } else {
+ mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
+ }
+ } else {
+ // Push a MethodHandle representing the hooked method.
+ val handleOpcode = when (opcode) {
+ Opcodes.INVOKEVIRTUAL -> Opcodes.H_INVOKEVIRTUAL
+ Opcodes.INVOKEINTERFACE -> Opcodes.H_INVOKEINTERFACE
+ Opcodes.INVOKESTATIC -> Opcodes.H_INVOKESTATIC
+ Opcodes.INVOKESPECIAL -> Opcodes.H_INVOKESPECIAL
+ else -> -1
+ }
+ if (java6Mode) {
+ // MethodHandle constants (type 15) are not supported in Java 6 class files (major version 50).
+ mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
+ } else {
+ mv.visitLdcInsn(
+ Handle(
+ handleOpcode,
+ owner,
+ methodName,
+ methodDescriptor,
+ isInterface
+ )
+ ) // push MethodHandle
+ }
+ // Stack layout: ... | MethodHandle (objectref)
+ // Push the owner object again
+ mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj)
+ }
+ // Stack layout: ... | MethodHandle (objectref) | owner (objectref)
+ // Push a reference to our object array with the saved arguments
+ mv.visitVarInsn(Opcodes.ALOAD, localObjArr)
+ // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref)
+ // Push the hook id
+ mv.visitLdcInsn(hookId)
+ // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int)
+ // How we proceed depends on the type of hook we want to implement
+ when (hook.hookType) {
+ HookType.BEFORE -> {
+ // Call the hook method
+ mv.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ hook.hookInternalClassName,
+ hook.hookMethodName,
+ hook.hookMethodDescriptor,
+ false
+ )
+ // Stack layout: ...
+ // Push the values for the original method call onto the stack again
+ if (opcode != Opcodes.INVOKESTATIC) {
+ mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object
+ }
+ loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments
+ // Stack layout: ... | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ...
+ // Call the original method or the next hook in order.
+ visitNextHookTypeOrCall(hookType, true, opcode, owner, methodName, methodDescriptor, isInterface)
+ }
+ HookType.REPLACE -> {
+ // Call the hook method
+ mv.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ hook.hookInternalClassName,
+ hook.hookMethodName,
+ hook.hookMethodDescriptor,
+ false
+ )
+ // Stack layout: ... | [return value (primitive/objectref)]
+ // Check if we need to process the return value
+ val returnTypeDescriptor = extractReturnTypeDescriptor(methodDescriptor)
+ if (returnTypeDescriptor != "V") {
+ val hookMethodReturnType = extractReturnTypeDescriptor(hook.hookMethodDescriptor)
+ // if the hook method's return type is primitive we don't need to unwrap or cast it
+ if (!isPrimitiveType(hookMethodReturnType)) {
+ // Check if the returned object type is different than the one that should be returned
+ // If a primitive should be returned we check it's wrapper type
+ val expectedType = getWrapperTypeDescriptor(returnTypeDescriptor)
+ if (expectedType != hookMethodReturnType) {
+ // Cast object
+ mv.visitTypeInsn(Opcodes.CHECKCAST, extractInternalClassName(expectedType))
+ }
+ // Check if we need to unwrap the returned object
+ unwrapTypeIfPrimitive(returnTypeDescriptor)
+ }
+ }
+ }
+ HookType.AFTER -> {
+ // Push the values for the original method call again onto the stack
+ if (opcode != Opcodes.INVOKESTATIC) {
+ mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object
+ }
+ loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments
+ // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int)
+ // | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ...
+ // Call the original method or the next hook in order
+ visitNextHookTypeOrCall(hookType, true, opcode, owner, methodName, methodDescriptor, isInterface)
+ val returnTypeDescriptor = extractReturnTypeDescriptor(methodDescriptor)
+ if (returnTypeDescriptor == "V") {
+ // If the method didn't return anything, we push a nullref as placeholder
+ mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
+ }
+ // Wrap return value if it is a primitive type
+ wrapTypeIfPrimitive(returnTypeDescriptor)
+ // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int)
+ // | return value (objectref)
+ // Store the result value in a local variable (but keep it on the stack)
+ val localReturnObj = lvs.newLocal(Type.getType(getWrapperTypeDescriptor(returnTypeDescriptor)))
+ mv.visitVarInsn(Opcodes.ASTORE, localReturnObj) // consume objectref
+ mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref
+ // Call the hook method
+ mv.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ hook.hookInternalClassName,
+ hook.hookMethodName,
+ hook.hookMethodDescriptor,
+ false
+ )
+ // Stack layout: ...
+ if (returnTypeDescriptor != "V") {
+ // Push the return value again
+ mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref
+ // Unwrap it, if it was a primitive value
+ unwrapTypeIfPrimitive(returnTypeDescriptor)
+ // Stack layout: ... | return value (primitive/objectref)
+ }
+ }
+ }
+ }
+
+ private fun isMethodInvocationOp(opcode: Int) = opcode in listOf(
+ Opcodes.INVOKEVIRTUAL,
+ Opcodes.INVOKEINTERFACE,
+ Opcodes.INVOKESTATIC,
+ Opcodes.INVOKESPECIAL
+ )
+
+ private fun findMatchingHook(hookType: HookType, owner: String, name: String, descriptor: String): Hook? {
+ val withoutDescriptorKey = "$hookType#$owner#$name"
+ val withDescriptorKey = "$withoutDescriptorKey#$descriptor"
+ return hooks[withDescriptorKey] ?: hooks[withoutDescriptorKey]
+ }
+
+ // Stores all arguments for a method call in a local object array.
+ // paramDescriptors: The type descriptors for all method arguments
+ private fun storeMethodArguments(paramDescriptors: List<String>): Int {
+ // Allocate a new Object[] for the methods parameters.
+ mv.visitIntInsn(Opcodes.SIPUSH, paramDescriptors.size)
+ mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object")
+ val localObjArr = lvs.newLocal(Type.getType("[Ljava/lang/Object;"))
+ mv.visitVarInsn(Opcodes.ASTORE, localObjArr)
+
+ // Loop over all arguments in reverse order (because the last argument is on top).
+ for ((argIdx, argDescriptor) in paramDescriptors.withIndex().reversed()) {
+ // If the argument is a primitive type, wrap it in it's wrapper class
+ wrapTypeIfPrimitive(argDescriptor)
+ // Store the argument in our object array, for that we need to shape the stack first.
+ // Stack layout: ... | method argument (objectref)
+ mv.visitVarInsn(Opcodes.ALOAD, localObjArr)
+ // Stack layout: ... | method argument (objectref) | object array (arrayref)
+ mv.visitInsn(Opcodes.SWAP)
+ // Stack layout: ... | object array (arrayref) | method argument (objectref)
+ mv.visitIntInsn(Opcodes.SIPUSH, argIdx)
+ // Stack layout: ... | object array (arrayref) | method argument (objectref) | argument index (int)
+ mv.visitInsn(Opcodes.SWAP)
+ // Stack layout: ... | object array (arrayref) | argument index (int) | method argument (objectref)
+ mv.visitInsn(Opcodes.AASTORE) // consume all three: arrayref, index, value
+ // Stack layout: ...
+ // Continue with the remaining method arguments
+ }
+
+ // Return a reference to the array with the parameters.
+ return localObjArr
+ }
+
+ // Loads all arguments for a method call from a local object array.
+ // argTypeSigs: The type signatures for all method arguments
+ // localObjArr: Index of a local variable containing an object array where the arguments will be loaded from
+ private fun loadMethodArguments(paramDescriptors: List<String>, localObjArr: Int) {
+ // Loop over all arguments
+ for ((argIdx, argDescriptor) in paramDescriptors.withIndex()) {
+ // Push a reference to the object array on the stack
+ mv.visitVarInsn(Opcodes.ALOAD, localObjArr)
+ // Stack layout: ... | object array (arrayref)
+ // Push the index of the current argument on the stack
+ mv.visitIntInsn(Opcodes.SIPUSH, argIdx)
+ // Stack layout: ... | object array (arrayref) | argument index (int)
+ // Load the argument from the array
+ mv.visitInsn(Opcodes.AALOAD)
+ // Stack layout: ... | method argument (objectref)
+ // Cast object to it's original type (or it's wrapper object)
+ val wrapperTypeDescriptor = getWrapperTypeDescriptor(argDescriptor)
+ mv.visitTypeInsn(Opcodes.CHECKCAST, extractInternalClassName(wrapperTypeDescriptor))
+ // If the argument is a supposed to be a primitive type, unwrap the wrapped type
+ unwrapTypeIfPrimitive(argDescriptor)
+ // Stack layout: ... | method argument (primitive/objectref)
+ // Continue with the remaining method arguments
+ }
+ }
+
+ // Removes a primitive value from the top of the operand stack
+ // and pushes it enclosed in it's wrapper type (e.g. removes int, pushes Integer).
+ // This is done by calling .valueOf(...) on the wrapper class.
+ private fun wrapTypeIfPrimitive(unwrappedTypeDescriptor: String) {
+ if (!isPrimitiveType(unwrappedTypeDescriptor) || unwrappedTypeDescriptor == "V") return
+ val wrapperTypeDescriptor = getWrapperTypeDescriptor(unwrappedTypeDescriptor)
+ val wrapperType = extractInternalClassName(wrapperTypeDescriptor)
+ val valueOfDescriptor = "($unwrappedTypeDescriptor)$wrapperTypeDescriptor"
+ mv.visitMethodInsn(Opcodes.INVOKESTATIC, wrapperType, "valueOf", valueOfDescriptor, false)
+ }
+
+ // Removes a wrapper object around a given primitive type from the top of the operand stack
+ // and pushes the primitive value it contains (e.g. removes Integer, pushes int).
+ // This is done by calling .intValue(...) / .charValue(...) / ... on the wrapper object.
+ private fun unwrapTypeIfPrimitive(primitiveTypeDescriptor: String) {
+ val (methodName, wrappedTypeDescriptor) = when (primitiveTypeDescriptor) {
+ "B" -> Pair("byteValue", "java/lang/Byte")
+ "C" -> Pair("charValue", "java/lang/Character")
+ "D" -> Pair("doubleValue", "java/lang/Double")
+ "F" -> Pair("floatValue", "java/lang/Float")
+ "I" -> Pair("intValue", "java/lang/Integer")
+ "J" -> Pair("longValue", "java/lang/Long")
+ "S" -> Pair("shortValue", "java/lang/Short")
+ "Z" -> Pair("booleanValue", "java/lang/Boolean")
+ else -> return
+ }
+ mv.visitMethodInsn(
+ Opcodes.INVOKEVIRTUAL,
+ wrappedTypeDescriptor,
+ methodName,
+ "()$primitiveTypeDescriptor",
+ false
+ )
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt
new file mode 100644
index 00000000..86ad45a3
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt
@@ -0,0 +1,45 @@
+// 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.instrumentor
+
+import com.code_intelligence.jazzer.third_party.objectweb.asm.Opcodes
+import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.MethodNode
+
+enum class InstrumentationType {
+ CMP,
+ COV,
+ DIV,
+ GEP,
+ INDIR,
+ NATIVE,
+}
+
+internal interface Instrumentor {
+ fun instrument(bytecode: ByteArray): ByteArray
+
+ fun shouldInstrument(access: Int): Boolean {
+ return (access and Opcodes.ACC_ABSTRACT == 0) &&
+ (access and Opcodes.ACC_NATIVE == 0)
+ }
+
+ fun shouldInstrument(method: MethodNode): Boolean {
+ return shouldInstrument(method.access) &&
+ method.instructions.size() > 0
+ }
+
+ companion object {
+ const val ASM_API_VERSION = Opcodes.ASM9
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt
new file mode 100644
index 00000000..e6d3176e
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt
@@ -0,0 +1,258 @@
+// 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.instrumentor
+
+import com.code_intelligence.jazzer.runtime.TraceDataFlowNativeCallbacks
+import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassReader
+import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassWriter
+import com.code_intelligence.jazzer.third_party.objectweb.asm.Opcodes
+import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.AbstractInsnNode
+import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.ClassNode
+import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.InsnList
+import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.InsnNode
+import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.IntInsnNode
+import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.LdcInsnNode
+import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.LookupSwitchInsnNode
+import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.MethodInsnNode
+import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.MethodNode
+import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.TableSwitchInsnNode
+
+internal class TraceDataFlowInstrumentor(private val types: Set<InstrumentationType>, callbackClass: Class<*> = TraceDataFlowNativeCallbacks::class.java) : Instrumentor {
+
+ private val callbackInternalClassName = callbackClass.name.replace('.', '/')
+ private lateinit var random: DeterministicRandom
+
+ override fun instrument(bytecode: ByteArray): ByteArray {
+ val node = ClassNode()
+ val reader = ClassReader(bytecode)
+ reader.accept(node, 0)
+ random = DeterministicRandom("trace", node.name)
+ for (method in node.methods) {
+ if (shouldInstrument(method)) {
+ addDataFlowInstrumentation(method)
+ }
+ }
+
+ val writer = ClassWriter(ClassWriter.COMPUTE_MAXS)
+ node.accept(writer)
+ return writer.toByteArray()
+ }
+
+ @OptIn(ExperimentalUnsignedTypes::class)
+ private fun addDataFlowInstrumentation(method: MethodNode) {
+ loop@ for (inst in method.instructions.toArray()) {
+ when (inst.opcode) {
+ Opcodes.LCMP -> {
+ if (InstrumentationType.CMP !in types) continue@loop
+ method.instructions.insertBefore(inst, longCmpInstrumentation())
+ method.instructions.remove(inst)
+ }
+ Opcodes.IF_ICMPEQ, Opcodes.IF_ICMPNE,
+ Opcodes.IF_ICMPLT, Opcodes.IF_ICMPLE,
+ Opcodes.IF_ICMPGT, Opcodes.IF_ICMPGE -> {
+ if (InstrumentationType.CMP !in types) continue@loop
+ method.instructions.insertBefore(inst, intCmpInstrumentation())
+ }
+ Opcodes.IFEQ, Opcodes.IFNE,
+ Opcodes.IFLT, Opcodes.IFLE,
+ Opcodes.IFGT, Opcodes.IFGE -> {
+ if (InstrumentationType.CMP !in types) continue@loop
+ // The IF* opcodes are often used to branch based on the result of a compare
+ // instruction for a type other than int. The operands of this compare will
+ // already be reported via the instrumentation above (for non-floating point
+ // numbers) and the follow-up compare does not provide a good signal as all
+ // operands will be in {-1, 0, 1}. Skip instrumentation for it.
+ if (inst.previous?.opcode in listOf(Opcodes.DCMPG, Opcodes.DCMPL, Opcodes.FCMPG, Opcodes.DCMPL) ||
+ (inst.previous as? MethodInsnNode)?.name == "traceCmpLongWrapper"
+ )
+ continue@loop
+ method.instructions.insertBefore(inst, ifInstrumentation())
+ }
+ Opcodes.LOOKUPSWITCH, Opcodes.TABLESWITCH -> {
+ if (InstrumentationType.CMP !in types) continue@loop
+ // Mimic the exclusion logic for small label values in libFuzzer:
+ // https://github.com/llvm-mirror/compiler-rt/blob/69445f095c22aac2388f939bedebf224a6efcdaf/lib/fuzzer/FuzzerTracePC.cpp#L520
+ // Case values are reported to libFuzzer via an array of unsigned long values and thus need to be
+ // sorted by unsigned value.
+ val caseValues = when (inst) {
+ is LookupSwitchInsnNode -> {
+ if (inst.keys.isEmpty() || (0 <= inst.keys.first() && inst.keys.last() < 256))
+ continue@loop
+ inst.keys
+ }
+ is TableSwitchInsnNode -> {
+ if (0 <= inst.min && inst.max < 256)
+ continue@loop
+ (inst.min..inst.max).filter { caseValue ->
+ val index = caseValue - inst.min
+ // Filter out "gap cases".
+ inst.labels[index].label != inst.dflt.label
+ }.toList()
+ }
+ // Not reached.
+ else -> continue@loop
+ }.sortedBy { it.toUInt() }.map { it.toLong() }.toLongArray()
+ method.instructions.insertBefore(inst, switchInstrumentation(caseValues))
+ }
+ Opcodes.IDIV -> {
+ if (InstrumentationType.DIV !in types) continue@loop
+ method.instructions.insertBefore(inst, intDivInstrumentation())
+ }
+ Opcodes.LDIV -> {
+ if (InstrumentationType.DIV !in types) continue@loop
+ method.instructions.insertBefore(inst, longDivInstrumentation())
+ }
+ Opcodes.AALOAD, Opcodes.BALOAD,
+ Opcodes.CALOAD, Opcodes.DALOAD,
+ Opcodes.FALOAD, Opcodes.IALOAD,
+ Opcodes.LALOAD, Opcodes.SALOAD -> {
+ if (InstrumentationType.GEP !in types) continue@loop
+ if (!isConstantIntegerPushInsn(inst.previous)) continue@loop
+ method.instructions.insertBefore(inst, gepLoadInstrumentation())
+ }
+ Opcodes.INVOKEINTERFACE, Opcodes.INVOKESPECIAL, Opcodes.INVOKESTATIC, Opcodes.INVOKEVIRTUAL -> {
+ if (InstrumentationType.GEP !in types) continue@loop
+ if (!isGepLoadMethodInsn(inst as MethodInsnNode)) continue@loop
+ if (!isConstantIntegerPushInsn(inst.previous)) continue@loop
+ method.instructions.insertBefore(inst, gepLoadInstrumentation())
+ }
+ }
+ }
+ }
+
+ private fun InsnList.pushFakePc() {
+ add(LdcInsnNode(random.nextInt(4096)))
+ }
+
+ private fun longCmpInstrumentation() = InsnList().apply {
+ pushFakePc()
+ // traceCmpLong returns the result of the comparison as duplicating two longs on the stack
+ // is not possible without local variables.
+ add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceCmpLongWrapper", "(JJI)I", false))
+ }
+
+ private fun intCmpInstrumentation() = InsnList().apply {
+ add(InsnNode(Opcodes.DUP2))
+ pushFakePc()
+ add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceCmpInt", "(III)V", false))
+ }
+
+ private fun ifInstrumentation() = InsnList().apply {
+ add(InsnNode(Opcodes.DUP))
+ // All if* instructions are compares to the constant 0.
+ add(InsnNode(Opcodes.ICONST_0))
+ add(InsnNode(Opcodes.SWAP))
+ pushFakePc()
+ add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceConstCmpInt", "(III)V", false))
+ }
+
+ private fun intDivInstrumentation() = InsnList().apply {
+ add(InsnNode(Opcodes.DUP))
+ pushFakePc()
+ add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceDivInt", "(II)V", false))
+ }
+
+ private fun longDivInstrumentation() = InsnList().apply {
+ add(InsnNode(Opcodes.DUP2))
+ pushFakePc()
+ add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceDivLong", "(JI)V", false))
+ }
+
+ private fun switchInstrumentation(caseValues: LongArray) = InsnList().apply {
+ // duplicate {lookup,table}switch key for use as first function argument
+ add(InsnNode(Opcodes.DUP))
+ add(InsnNode(Opcodes.I2L))
+ // Set up array with switch case values. The format libfuzzer expects is created here directly, i.e., the first
+ // two entries are the number of cases and the bit size of values (always 32).
+ add(IntInsnNode(Opcodes.SIPUSH, caseValues.size + 2))
+ add(IntInsnNode(Opcodes.NEWARRAY, Opcodes.T_LONG))
+ // Store number of cases
+ add(InsnNode(Opcodes.DUP))
+ add(IntInsnNode(Opcodes.SIPUSH, 0))
+ add(LdcInsnNode(caseValues.size.toLong()))
+ add(InsnNode(Opcodes.LASTORE))
+ // Store bit size of keys
+ add(InsnNode(Opcodes.DUP))
+ add(IntInsnNode(Opcodes.SIPUSH, 1))
+ add(LdcInsnNode(32.toLong()))
+ add(InsnNode(Opcodes.LASTORE))
+ // Store {lookup,table}switch case values
+ for ((i, caseValue) in caseValues.withIndex()) {
+ add(InsnNode(Opcodes.DUP))
+ add(IntInsnNode(Opcodes.SIPUSH, 2 + i))
+ add(LdcInsnNode(caseValue))
+ add(InsnNode(Opcodes.LASTORE))
+ }
+ pushFakePc()
+ // call the native callback function
+ add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceSwitch", "(J[JI)V", false))
+ }
+
+ /**
+ * Returns true if [node] represents an instruction that possibly pushes a valid, non-zero, constant array index
+ * onto the stack.
+ */
+ private fun isConstantIntegerPushInsn(node: AbstractInsnNode?) = node?.opcode in CONSTANT_INTEGER_PUSH_OPCODES
+
+ /**
+ * Returns true if [node] represents a call to a method that performs an indexed lookup into an array-like
+ * structure.
+ */
+ private fun isGepLoadMethodInsn(node: MethodInsnNode): Boolean {
+ if (!node.desc.startsWith("(I)")) return false
+ val returnType = node.desc.removePrefix("(I)")
+ return MethodInfo(node.owner, node.name, returnType) in GEP_LOAD_METHODS
+ }
+
+ private fun gepLoadInstrumentation() = InsnList().apply {
+ // Duplicate the index and convert to long.
+ add(InsnNode(Opcodes.DUP))
+ add(InsnNode(Opcodes.I2L))
+ pushFakePc()
+ add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceGep", "(JI)V", false))
+ }
+
+ companion object {
+ // Low constants (0, 1) are omitted as they create a lot of noise.
+ val CONSTANT_INTEGER_PUSH_OPCODES = listOf(
+ Opcodes.BIPUSH, Opcodes.SIPUSH,
+ Opcodes.LDC,
+ Opcodes.ICONST_2, Opcodes.ICONST_3, Opcodes.ICONST_4, Opcodes.ICONST_5
+ )
+
+ data class MethodInfo(val internalClassName: String, val name: String, val returnType: String)
+
+ val GEP_LOAD_METHODS = setOf(
+ MethodInfo("java/util/AbstractList", "get", "Ljava/lang/Object;"),
+ MethodInfo("java/util/ArrayList", "get", "Ljava/lang/Object;"),
+ MethodInfo("java/util/List", "get", "Ljava/lang/Object;"),
+ MethodInfo("java/util/Stack", "get", "Ljava/lang/Object;"),
+ MethodInfo("java/util/Vector", "get", "Ljava/lang/Object;"),
+ MethodInfo("java/lang/CharSequence", "charAt", "C"),
+ MethodInfo("java/lang/String", "charAt", "C"),
+ MethodInfo("java/lang/StringBuffer", "charAt", "C"),
+ MethodInfo("java/lang/StringBuilder", "charAt", "C"),
+ MethodInfo("java/lang/String", "codePointAt", "I"),
+ MethodInfo("java/lang/String", "codePointBefore", "I"),
+ MethodInfo("java/nio/ByteBuffer", "get", "B"),
+ MethodInfo("java/nio/ByteBuffer", "getChar", "C"),
+ MethodInfo("java/nio/ByteBuffer", "getDouble", "D"),
+ MethodInfo("java/nio/ByteBuffer", "getFloat", "F"),
+ MethodInfo("java/nio/ByteBuffer", "getInt", "I"),
+ MethodInfo("java/nio/ByteBuffer", "getLong", "J"),
+ MethodInfo("java/nio/ByteBuffer", "getShort", "S"),
+ )
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/shade_rules b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/shade_rules
new file mode 100644
index 00000000..c2092b3b
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/shade_rules
@@ -0,0 +1 @@
+rule org.** com.code_intelligence.jazzer.third_party.@1 \ No newline at end of file
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel
new file mode 100644
index 00000000..df28adb4
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library")
+
+java_jni_library(
+ name = "replay",
+ srcs = ["Replayer.java"],
+ native_libs = ["//agent/src/main/native/com/code_intelligence/jazzer/replay"],
+ visibility = ["//agent/src/main/native/com/code_intelligence/jazzer/replay:__pkg__"],
+ deps = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/api",
+ "//agent/src/main/java/com/code_intelligence/jazzer/runtime:fuzzed_data_provider",
+ ],
+)
+
+java_binary(
+ name = "Replayer",
+ visibility = ["//visibility:public"],
+ runtime_deps = [":replay"],
+)
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java b/agent/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java
new file mode 100644
index 00000000..fc6bfc4f
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java
@@ -0,0 +1,159 @@
+// 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.replay;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.runtime.FuzzedDataProviderImpl;
+import com.github.fmeum.rules_jni.RulesJni;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+
+public class Replayer {
+ public static final int STATUS_FINDING = 77;
+ public static final int STATUS_OTHER_ERROR = 1;
+
+ static {
+ try {
+ RulesJni.loadLibrary("replay", Replayer.class);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ System.exit(STATUS_OTHER_ERROR);
+ }
+ }
+
+ public static void main(String[] args) {
+ if (args.length < 2) {
+ System.err.println("Usage: <fuzz target class> <input file path> <fuzzerInitialize args>...");
+ System.exit(STATUS_OTHER_ERROR);
+ }
+ ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true);
+
+ Class<?> fuzzTargetClass;
+ try {
+ fuzzTargetClass = Class.forName(args[0]);
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ System.exit(STATUS_OTHER_ERROR);
+ // Without this return the compiler sees fuzzTargetClass as possibly uninitialized.
+ return;
+ }
+
+ String inputFilePath = args[1];
+ byte[] input = loadInput(inputFilePath);
+
+ String[] fuzzTargetArgs = Arrays.copyOfRange(args, 2, args.length);
+ executeFuzzerInitialize(fuzzTargetClass, fuzzTargetArgs);
+ executeFuzzTarget(fuzzTargetClass, input);
+ }
+
+ private static byte[] loadInput(String inputFilePath) {
+ try {
+ return Files.readAllBytes(Paths.get(inputFilePath));
+ } catch (IOException e) {
+ e.printStackTrace();
+ System.exit(STATUS_OTHER_ERROR);
+ // Without this return the compiler sees loadInput as possibly not returning a value.
+ return null;
+ }
+ }
+
+ private static void executeFuzzerInitialize(Class<?> fuzzTarget, String[] args) {
+ // public static void fuzzerInitialize()
+ try {
+ Method fuzzerInitialize = fuzzTarget.getMethod("fuzzerInitialize");
+ fuzzerInitialize.invoke(null);
+ return;
+ } catch (Exception e) {
+ handleInvokeException(e, fuzzTarget);
+ }
+ // public static void fuzzerInitialize(String[] args)
+ try {
+ Method fuzzerInitialize = fuzzTarget.getMethod("fuzzerInitialize", String[].class);
+ fuzzerInitialize.invoke(null, (Object) args);
+ } catch (Exception e) {
+ handleInvokeException(e, fuzzTarget);
+ }
+ }
+
+ public static void executeFuzzTarget(Class<?> fuzzTarget, byte[] input) {
+ // public static void fuzzerTestOneInput(byte[] input)
+ try {
+ Method fuzzerTestOneInput = fuzzTarget.getMethod("fuzzerTestOneInput", byte[].class);
+ fuzzerTestOneInput.invoke(null, (Object) input);
+ return;
+ } catch (Exception e) {
+ handleInvokeException(e, fuzzTarget);
+ }
+ // public static void fuzzerTestOneInput(FuzzedDataProvider data)
+ try {
+ Method fuzzerTestOneInput =
+ fuzzTarget.getMethod("fuzzerTestOneInput", FuzzedDataProvider.class);
+ fuzzerTestOneInput.invoke(null, makeFuzzedDataProvider(input));
+ return;
+ } catch (Exception e) {
+ handleInvokeException(e, fuzzTarget);
+ }
+ System.err.printf("%s must define exactly one of the following two functions:%n"
+ + " public static void fuzzerTestOneInput(byte[] ...)%n"
+ + " public static void fuzzerTestOneInput(FuzzedDataProvider ...)%n"
+ + "Note: Fuzz targets returning boolean are no longer supported; exceptions should%n"
+ + "be thrown instead of returning true.%n",
+ fuzzTarget.getName());
+ System.exit(STATUS_OTHER_ERROR);
+ }
+
+ private static void handleInvokeException(Exception e, Class<?> fuzzTarget) {
+ if (e instanceof NoSuchMethodException)
+ return;
+ if (e instanceof InvocationTargetException) {
+ filterOutOwnStackTraceElements(e.getCause(), fuzzTarget);
+ e.getCause().printStackTrace();
+ System.exit(STATUS_FINDING);
+ } else {
+ e.printStackTrace();
+ System.exit(STATUS_OTHER_ERROR);
+ }
+ }
+
+ private static void filterOutOwnStackTraceElements(Throwable t, Class<?> fuzzTarget) {
+ if (t.getCause() != null)
+ filterOutOwnStackTraceElements(t.getCause(), fuzzTarget);
+ if (t.getStackTrace() == null || t.getStackTrace().length == 0)
+ return;
+ StackTraceElement lowestFrame = t.getStackTrace()[t.getStackTrace().length - 1];
+ if (!lowestFrame.getClassName().equals(Replayer.class.getName())
+ || !lowestFrame.getMethodName().equals("main"))
+ return;
+ for (int i = t.getStackTrace().length - 1; i >= 0; i--) {
+ StackTraceElement frame = t.getStackTrace()[i];
+ if (frame.getClassName().equals(fuzzTarget.getName())
+ && frame.getMethodName().equals("fuzzerTestOneInput")) {
+ t.setStackTrace(Arrays.copyOfRange(t.getStackTrace(), 0, i + 1));
+ break;
+ }
+ }
+ }
+
+ private static FuzzedDataProvider makeFuzzedDataProvider(byte[] input) {
+ feedFuzzedDataProvider(input);
+ return new FuzzedDataProviderImpl();
+ }
+
+ private static native void feedFuzzedDataProvider(byte[] input);
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel
new file mode 100644
index 00000000..095b0bf8
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel
@@ -0,0 +1,48 @@
+load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library")
+
+java_library(
+ name = "fuzzed_data_provider",
+ srcs = [
+ "FuzzedDataProviderImpl.java",
+ ],
+ visibility = ["//agent/src/main/java/com/code_intelligence/jazzer/replay:__pkg__"],
+ deps = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/api",
+ ],
+)
+
+java_library(
+ name = "signal_handler",
+ srcs = ["SignalHandler.java"],
+ javacopts = [
+ "-XDenableSunApiLintControl",
+ ],
+)
+
+kt_jvm_library(
+ name = "runtime",
+ srcs = [
+ "CoverageMap.java",
+ "ExceptionUtils.kt",
+ "HardToCatchError.java",
+ "JazzerInternal.java",
+ "ManifestUtils.kt",
+ "NativeLibHooks.java",
+ "RecordingFuzzedDataProvider.java",
+ "SignalHandler.java",
+ "TraceCmpHooks.java",
+ "TraceDataFlowNativeCallbacks.java",
+ "TraceDivHooks.java",
+ "TraceIndirHooks.java",
+ ],
+ visibility = ["//visibility:public"],
+ runtime_deps = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/autofuzz",
+ ],
+ deps = [
+ ":fuzzed_data_provider",
+ ":signal_handler",
+ "//agent/src/main/java/com/code_intelligence/jazzer/api",
+ "//agent/src/main/java/com/code_intelligence/jazzer/utils",
+ ],
+)
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java
new file mode 100644
index 00000000..af2424a2
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java
@@ -0,0 +1,33 @@
+// 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.runtime;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Represents the Java view on a libFuzzer 8 bit counter coverage map.
+ * By using a direct ByteBuffer, the counter array is shared directly with
+ * native code.
+ */
+final public class CoverageMap {
+ public static ByteBuffer mem = ByteBuffer.allocateDirect(0);
+
+ public static void enlargeCoverageMap() {
+ registerNewCoverageCounters();
+ System.out.println("INFO: New number of inline 8-bit counters: " + mem.capacity());
+ }
+
+ private static native void registerNewCoverageCounters();
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/ExceptionUtils.kt b/agent/src/main/java/com/code_intelligence/jazzer/runtime/ExceptionUtils.kt
new file mode 100644
index 00000000..31a61740
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/ExceptionUtils.kt
@@ -0,0 +1,166 @@
+// 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.runtime
+
+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()
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImpl.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImpl.java
new file mode 100644
index 00000000..fe4d8ac7
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImpl.java
@@ -0,0 +1,83 @@
+// 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.runtime;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+public class FuzzedDataProviderImpl implements FuzzedDataProvider {
+ public FuzzedDataProviderImpl() {}
+
+ @Override public native boolean consumeBoolean();
+
+ @Override public native boolean[] consumeBooleans(int maxLength);
+
+ @Override public native byte consumeByte();
+
+ @Override public native byte consumeByte(byte min, byte max);
+
+ @Override public native short consumeShort();
+
+ @Override public native short consumeShort(short min, short max);
+
+ @Override public native short[] consumeShorts(int maxLength);
+
+ @Override public native int consumeInt();
+
+ @Override public native int consumeInt(int min, int max);
+
+ @Override public native int[] consumeInts(int maxLength);
+
+ @Override public native long consumeLong();
+
+ @Override public native long consumeLong(long min, long max);
+
+ @Override public native long[] consumeLongs(int maxLength);
+
+ @Override public native float consumeFloat();
+
+ @Override public native float consumeRegularFloat();
+
+ @Override public native float consumeRegularFloat(float min, float max);
+
+ @Override public native float consumeProbabilityFloat();
+
+ @Override public native double consumeDouble();
+
+ @Override public native double consumeRegularDouble(double min, double max);
+
+ @Override public native double consumeRegularDouble();
+
+ @Override public native double consumeProbabilityDouble();
+
+ @Override public native char consumeChar();
+
+ @Override public native char consumeChar(char min, char max);
+
+ @Override public native char consumeCharNoSurrogates();
+
+ @Override public native String consumeAsciiString(int maxLength);
+
+ @Override public native String consumeString(int maxLength);
+
+ @Override public native String consumeRemainingAsAsciiString();
+
+ @Override public native String consumeRemainingAsString();
+
+ @Override public native byte[] consumeBytes(int maxLength);
+
+ @Override public native byte[] consumeRemainingAsBytes();
+
+ @Override public native int remainingBytes();
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/HardToCatchError.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/HardToCatchError.java
new file mode 100644
index 00000000..cf136051
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/HardToCatchError.java
@@ -0,0 +1,82 @@
+// 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.runtime;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+
+/**
+ * An Error that rethrows itself when any of its getters is invoked.
+ */
+public class HardToCatchError extends Error {
+ public HardToCatchError() {
+ super();
+ }
+
+ @Override
+ public String getMessage() {
+ throw this;
+ }
+
+ @Override
+ public String getLocalizedMessage() {
+ throw this;
+ }
+
+ @Override
+ public synchronized Throwable initCause(Throwable cause) {
+ throw this;
+ }
+
+ @Override
+ public String toString() {
+ throw this;
+ }
+
+ @Override
+ public void printStackTrace() {
+ throw this;
+ }
+
+ @Override
+ public void printStackTrace(PrintStream s) {
+ throw this;
+ }
+
+ @Override
+ public void printStackTrace(PrintWriter s) {
+ throw this;
+ }
+
+ @Override
+ public StackTraceElement[] getStackTrace() {
+ throw this;
+ }
+
+ @Override
+ public int hashCode() {
+ throw this;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ throw this;
+ }
+
+ @Override
+ public Object clone() {
+ throw this;
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java
new file mode 100644
index 00000000..8bc1b38c
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java
@@ -0,0 +1,29 @@
+// 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.runtime;
+
+final public class JazzerInternal {
+ // Accessed from native code.
+ private static Throwable lastFinding;
+
+ // Accessed from api.Jazzer via reflection.
+ public static void reportFindingFromHook(Throwable finding) {
+ lastFinding = finding;
+ // Throw an Error that is hard to catch (short of outright ignoring it) in order to quickly
+ // terminate the execution of the fuzz target. The finding will be reported as soon as the fuzz
+ // target returns even if this Error is swallowed.
+ throw new HardToCatchError();
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/ManifestUtils.kt b/agent/src/main/java/com/code_intelligence/jazzer/runtime/ManifestUtils.kt
new file mode 100644
index 00000000..d88c3e18
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/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.runtime
+
+import java.util.jar.Manifest
+
+object ManifestUtils {
+
+ 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/runtime/NativeLibHooks.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/NativeLibHooks.java
new file mode 100644
index 00000000..495cad7c
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/NativeLibHooks.java
@@ -0,0 +1,35 @@
+// 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.runtime;
+
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+
+@SuppressWarnings("unused")
+final public class NativeLibHooks {
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Runtime",
+ targetMethod = "loadLibrary", targetMethodDescriptor = "(Ljava/lang/String;)V")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System",
+ targetMethod = "loadLibrary", targetMethodDescriptor = "(Ljava/lang/String;)V")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Runtime", targetMethod = "load",
+ targetMethodDescriptor = "(Ljava/lang/String;)V")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", targetMethod = "load",
+ targetMethodDescriptor = "(Ljava/lang/String;)V")
+ public static void
+ loadLibraryHook(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ TraceDataFlowNativeCallbacks.handleLibraryLoad();
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProvider.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProvider.java
new file mode 100644
index 00000000..976e024c
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProvider.java
@@ -0,0 +1,74 @@
+// 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.runtime;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Base64;
+
+// Wraps the native FuzzedDataProviderImpl and serializes all its return values
+// into a Base64-encoded string.
+final class RecordingFuzzedDataProvider implements InvocationHandler {
+ private final FuzzedDataProvider target = new FuzzedDataProviderImpl();
+ private final ArrayList<Object> recordedReplies = new ArrayList<>();
+
+ private RecordingFuzzedDataProvider() {}
+
+ // Called from native code.
+ public static FuzzedDataProvider makeFuzzedDataProviderProxy() {
+ return (FuzzedDataProvider) Proxy.newProxyInstance(
+ RecordingFuzzedDataProvider.class.getClassLoader(), new Class[] {FuzzedDataProvider.class},
+ new RecordingFuzzedDataProvider());
+ }
+
+ // Called from native code.
+ public static String serializeFuzzedDataProviderProxy(FuzzedDataProvider proxy)
+ throws IOException {
+ return ((RecordingFuzzedDataProvider) Proxy.getInvocationHandler(proxy)).serialize();
+ }
+
+ private Object recordAndReturn(Object object) {
+ recordedReplies.add(object);
+ return object;
+ }
+
+ @Override
+ public Object invoke(Object object, Method method, Object[] args) throws Throwable {
+ if (method.isDefault()) {
+ // Default methods in FuzzedDataProvider are implemented in Java and
+ // don't need to be recorded.
+ return method.invoke(target, args);
+ } else {
+ return recordAndReturn(method.invoke(target, args));
+ }
+ }
+
+ private String serialize() throws IOException {
+ byte[] rawOut;
+ try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream()) {
+ try (ObjectOutputStream objectStream = new ObjectOutputStream(byteStream)) {
+ objectStream.writeObject(recordedReplies);
+ }
+ rawOut = byteStream.toByteArray();
+ }
+ return Base64.getEncoder().encodeToString(rawOut);
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/SignalHandler.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/SignalHandler.java
new file mode 100644
index 00000000..0a42aa94
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/SignalHandler.java
@@ -0,0 +1,26 @@
+// 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.runtime;
+
+import sun.misc.Signal;
+
+@SuppressWarnings({"unused", "sunapi"})
+final class SignalHandler {
+ public static native void handleInterrupt();
+
+ public static void setupSignalHandlers() {
+ Signal.handle(new Signal("INT"), sig -> handleInterrupt());
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java
new file mode 100644
index 00000000..352da8ea
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java
@@ -0,0 +1,330 @@
+// 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.runtime;
+
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.TreeMap;
+
+@SuppressWarnings("unused")
+final public class TraceCmpHooks {
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Byte", targetMethod = "compare",
+ targetMethodDescriptor = "(BB)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Byte",
+ targetMethod = "compareUnsigned", targetMethodDescriptor = "(BB)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Short", targetMethod = "compare",
+ targetMethodDescriptor = "(SS)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Short",
+ targetMethod = "compareUnsigned", targetMethodDescriptor = "(SS)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer",
+ targetMethod = "compare", targetMethodDescriptor = "(II)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer",
+ targetMethod = "compareUnsigned", targetMethodDescriptor = "(II)I")
+ public static void
+ integerCompare(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ TraceDataFlowNativeCallbacks.traceCmpInt((int) arguments[0], (int) arguments[1], hookId);
+ }
+
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Byte",
+ targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Byte;)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Short",
+ targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Short;)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer",
+ targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Integer;)I")
+ public static void
+ integerCompareTo(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ TraceDataFlowNativeCallbacks.traceCmpInt((int) thisObject, (int) arguments[0], hookId);
+ }
+
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long", targetMethod = "compare",
+ targetMethodDescriptor = "(JJ)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long",
+ targetMethod = "compareUnsigned", targetMethodDescriptor = "(JJ)I")
+ public static void
+ longCompare(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ TraceDataFlowNativeCallbacks.traceCmpLong((long) arguments[0], (long) arguments[1], hookId);
+ }
+
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long",
+ targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Long;)I")
+ public static void
+ longCompareTo(MethodHandle method, Long thisObject, Object[] arguments, int hookId) {
+ TraceDataFlowNativeCallbacks.traceCmpLong(thisObject, (long) arguments[0], hookId);
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String",
+ targetMethod = "equalsIgnoreCase")
+ public static void
+ equals(
+ MethodHandle method, String thisObject, Object[] arguments, int hookId, Boolean returnValue) {
+ if (arguments[0] instanceof String && !returnValue) {
+ // The precise value of the result of the comparison is not used by libFuzzer as long as it is
+ // non-zero.
+ TraceDataFlowNativeCallbacks.traceStrcmp(thisObject, (String) arguments[0], 1, hookId);
+ }
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "compareTo")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String",
+ targetMethod = "compareToIgnoreCase")
+ public static void
+ compareTo(
+ MethodHandle method, String thisObject, Object[] arguments, int hookId, Integer returnValue) {
+ if (arguments[0] instanceof String && returnValue != 0) {
+ TraceDataFlowNativeCallbacks.traceStrcmp(
+ thisObject, (String) arguments[0], returnValue, hookId);
+ }
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "contentEquals")
+ public static void
+ contentEquals(
+ MethodHandle method, String thisObject, Object[] arguments, int hookId, Boolean returnValue) {
+ if (arguments[0] instanceof CharSequence && !returnValue) {
+ TraceDataFlowNativeCallbacks.traceStrcmp(
+ thisObject, ((CharSequence) arguments[0]).toString(), 1, hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String",
+ targetMethod = "regionMatches", targetMethodDescriptor = "(ZILjava/lang/String;II)Z")
+ public static void
+ regionsMatches5(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) {
+ if (!returnValue) {
+ int toffset = (int) arguments[1];
+ String other = (String) arguments[2];
+ int ooffset = (int) arguments[3];
+ int len = (int) arguments[4];
+ regionMatchesInternal((String) thisObject, toffset, other, ooffset, len, hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String",
+ targetMethod = "regionMatches", targetMethodDescriptor = "(ILjava/lang/String;II)Z")
+ public static void
+ regionMatches4(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) {
+ if (!returnValue) {
+ int toffset = (int) arguments[0];
+ String other = (String) arguments[1];
+ int ooffset = (int) arguments[2];
+ int len = (int) arguments[3];
+ regionMatchesInternal((String) thisObject, toffset, other, ooffset, len, hookId);
+ }
+ }
+
+ private static void regionMatchesInternal(
+ String thisString, int toffset, String other, int ooffset, int len, int hookId) {
+ if (toffset < 0 || ooffset < 0)
+ return;
+ int cappedThisStringEnd = Math.min(toffset + len, thisString.length());
+ int cappedOtherStringEnd = Math.min(ooffset + len, other.length());
+ String thisPart = thisString.substring(toffset, cappedThisStringEnd);
+ String otherPart = other.substring(ooffset, cappedOtherStringEnd);
+ TraceDataFlowNativeCallbacks.traceStrcmp(thisPart, otherPart, 1, hookId);
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "contains")
+ public static void
+ contains(
+ MethodHandle method, String thisObject, Object[] arguments, int hookId, Boolean returnValue) {
+ if (arguments[0] instanceof CharSequence && !returnValue) {
+ TraceDataFlowNativeCallbacks.traceStrstr(
+ thisObject, ((CharSequence) arguments[0]).toString(), hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "indexOf")
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "lastIndexOf")
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.StringBuffer", targetMethod = "indexOf")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.StringBuffer",
+ targetMethod = "lastIndexOf")
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.StringBuilder", targetMethod = "indexOf")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.StringBuilder",
+ targetMethod = "lastIndexOf")
+ public static void
+ indexOf(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Integer returnValue) {
+ if (arguments[0] instanceof String && returnValue == -1) {
+ TraceDataFlowNativeCallbacks.traceStrstr(
+ thisObject.toString(), (String) arguments[0], hookId);
+ }
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "startsWith")
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "endsWith")
+ public static void
+ startsWith(
+ MethodHandle method, String thisObject, Object[] arguments, int hookId, Boolean returnValue) {
+ if (!returnValue) {
+ TraceDataFlowNativeCallbacks.traceStrstr(thisObject, (String) arguments[0], hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "replace",
+ targetMethodDescriptor =
+ "(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;")
+ public static void
+ replace(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, String returnValue) {
+ String original = (String) thisObject;
+ String target = arguments[0].toString();
+ // Report only if the replacement was not successful.
+ if (original.equals(returnValue)) {
+ TraceDataFlowNativeCallbacks.traceStrstr(original, target, hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "equals",
+ targetMethodDescriptor = "([B[B)Z")
+ public static void
+ arraysEquals(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) {
+ byte[] first = (byte[]) arguments[0];
+ byte[] second = (byte[]) arguments[1];
+ if (!returnValue) {
+ TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "equals",
+ targetMethodDescriptor = "([BII[BII)Z")
+ public static void
+ arraysEqualsRange(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) {
+ byte[] first =
+ Arrays.copyOfRange((byte[]) arguments[0], (int) arguments[1], (int) arguments[2]);
+ byte[] second =
+ Arrays.copyOfRange((byte[]) arguments[3], (int) arguments[4], (int) arguments[5]);
+ if (!returnValue) {
+ TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "compare",
+ targetMethodDescriptor = "([B[B)I")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays",
+ targetMethod = "compareUnsigned", targetMethodDescriptor = "([B[B)I")
+ public static void
+ arraysCompare(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Integer returnValue) {
+ byte[] first = (byte[]) arguments[0];
+ byte[] second = (byte[]) arguments[1];
+ if (returnValue != 0) {
+ TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId);
+ }
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "compare",
+ targetMethodDescriptor = "([BII[BII)I")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays",
+ targetMethod = "compareUnsigned", targetMethodDescriptor = "([BII[BII)I")
+ public static void
+ arraysCompareRange(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Integer returnValue) {
+ byte[] first =
+ Arrays.copyOfRange((byte[]) arguments[0], (int) arguments[1], (int) arguments[2]);
+ byte[] second =
+ Arrays.copyOfRange((byte[]) arguments[3], (int) arguments[4], (int) arguments[5]);
+ if (returnValue != 0) {
+ TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId);
+ }
+ }
+
+ // The maximal number of elements of a non-TreeMap Map that will be sorted and searched for the
+ // key closest to the current lookup key in the mapGet hook.
+ private static final int MAX_NUM_KEYS_TO_ENUMERATE = 100;
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "com.google.common.collect.ImmutableMap",
+ targetMethod = "get")
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.util.AbstractMap", targetMethod = "get")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.EnumMap", targetMethod = "get")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.HashMap", targetMethod = "get")
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.util.LinkedHashMap", targetMethod = "get")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Map", targetMethod = "get")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.SortedMap", targetMethod = "get")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.TreeMap", targetMethod = "get")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.util.concurrent.ConcurrentMap",
+ targetMethod = "get")
+ public static void
+ mapGet(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) {
+ if (returnValue != null)
+ return;
+ if (thisObject == null)
+ return;
+ final Map map = (Map) thisObject;
+ if (map.size() == 0)
+ return;
+ final Object currentKey = arguments[0];
+ if (currentKey == null)
+ return;
+ // Find two valid map keys that bracket currentKey.
+ // This is a generalization of libFuzzer's __sanitizer_cov_trace_switch:
+ // https://github.com/llvm/llvm-project/blob/318942de229beb3b2587df09e776a50327b5cef0/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp#L564
+ Object lowerBoundKey = null;
+ Object upperBoundKey = null;
+ if (map instanceof TreeMap) {
+ final TreeMap treeMap = (TreeMap) map;
+ lowerBoundKey = treeMap.floorKey(currentKey);
+ upperBoundKey = treeMap.ceilingKey(currentKey);
+ } else if (currentKey instanceof Comparable) {
+ final Comparable comparableKey = (Comparable) currentKey;
+ // Find two keys that bracket currentKey.
+ // Note: This is not deterministic if map.size() > MAX_NUM_KEYS_TO_ENUMERATE.
+ int enumeratedKeys = 0;
+ for (Object validKey : map.keySet()) {
+ if (validKey == null)
+ continue;
+ // If the key sorts lower than the non-existing key, but higher than the current lower
+ // bound, update the lower bound and vice versa for the upper bound.
+ if (comparableKey.compareTo(validKey) > 0
+ && (lowerBoundKey == null || ((Comparable) validKey).compareTo(lowerBoundKey) > 0)) {
+ lowerBoundKey = validKey;
+ }
+ if (comparableKey.compareTo(validKey) < 0
+ && (upperBoundKey == null || ((Comparable) validKey).compareTo(upperBoundKey) < 0)) {
+ upperBoundKey = validKey;
+ }
+ if (enumeratedKeys++ > MAX_NUM_KEYS_TO_ENUMERATE)
+ break;
+ }
+ }
+ // Modify the hook ID so that compares against distinct valid keys are traced separately.
+ if (lowerBoundKey != null) {
+ TraceDataFlowNativeCallbacks.traceGenericCmp(
+ currentKey, lowerBoundKey, hookId + lowerBoundKey.hashCode());
+ }
+ if (upperBoundKey != null) {
+ TraceDataFlowNativeCallbacks.traceGenericCmp(
+ currentKey, upperBoundKey, hookId + upperBoundKey.hashCode());
+ }
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java
new file mode 100644
index 00000000..456d0cb9
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java
@@ -0,0 +1,91 @@
+// 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.runtime;
+
+import com.code_intelligence.jazzer.utils.Utils;
+import java.lang.reflect.Executable;
+
+@SuppressWarnings("unused")
+final public class TraceDataFlowNativeCallbacks {
+ /* trace-cmp */
+ // Calls: void __sanitizer_cov_trace_cmp4(uint32_t Arg1, uint32_t Arg2);
+ public static native void traceCmpInt(int arg1, int arg2, int pc);
+
+ // Calls: void __sanitizer_cov_trace_const_cmp4(uint32_t Arg1, uint32_t Arg2);
+ public static native void traceConstCmpInt(int arg1, int arg2, int pc);
+
+ // Calls: void __sanitizer_cov_trace_cmp4(uint32_t Arg1, uint32_t Arg2);
+ public static native void traceCmpLong(long arg1, long arg2, int pc);
+
+ // Calls: void __sanitizer_cov_trace_switch(uint64_t Val, uint64_t *Cases);
+ public static native void traceSwitch(long val, long[] cases, int pc);
+
+ // Calls: void __sanitizer_weak_hook_memcmp(void *caller_pc, const void *b1, const void *b2,
+ // size_t n, int result);
+ public static native void traceMemcmp(byte[] b1, byte[] b2, int result, int pc);
+
+ // Calls: void __sanitizer_weak_hook_strcmp(void *called_pc, const char *s1, const char *s2, int
+ // result);
+ public static native void traceStrcmp(String s1, String s2, int result, int pc);
+
+ // Calls: void __sanitizer_weak_hook_strstr(void *called_pc, const char *s1, const char *s2, char
+ // *result);
+ public static native void traceStrstr(String s1, String s2, int pc);
+
+ /* trace-div */
+ // Calls: void __sanitizer_cov_trace_div4(uint32_t Val);
+ public static native void traceDivInt(int val, int pc);
+
+ // Calls: void __sanitizer_cov_trace_div8(uint64_t Val);
+ public static native void traceDivLong(long val, int pc);
+
+ /* trace-gep */
+ // Calls: void __sanitizer_cov_trace_gep(uintptr_t Idx);
+ public static native void traceGep(long val, int pc);
+
+ /* indirect-calls */
+ // Calls: void __sanitizer_cov_trace_pc_indir(uintptr_t Callee);
+ private static native void tracePcIndir(int callee, int caller);
+
+ public static void traceReflectiveCall(Executable callee, int pc) {
+ String className = callee.getDeclaringClass().getCanonicalName();
+ String executableName = callee.getName();
+ String descriptor = Utils.getDescriptor(callee);
+ tracePcIndir(Utils.simpleFastHash(className, executableName, descriptor), pc);
+ }
+
+ public static int traceCmpLongWrapper(long arg1, long arg2, int pc) {
+ traceCmpLong(arg1, arg2, pc);
+ // Long.compare serves as a substitute for the lcmp opcode, which can't be used directly
+ // as the stack layout required for the call can't be achieved without local variables.
+ return Long.compare(arg1, arg2);
+ }
+
+ // The caller has to ensure that arg1 and arg2 have the same class.
+ public static void traceGenericCmp(Object arg1, Object arg2, int pc) {
+ if (arg1 instanceof String) {
+ traceStrcmp((String) arg1, (String) arg2, 1, pc);
+ } else if (arg1 instanceof Integer || arg1 instanceof Short || arg1 instanceof Byte
+ || arg1 instanceof Character) {
+ traceCmpInt((int) arg1, (int) arg2, pc);
+ } else if (arg1 instanceof Long) {
+ traceCmpLong((long) arg1, (long) arg2, pc);
+ } else if (arg1 instanceof byte[]) {
+ traceMemcmp((byte[]) arg1, (byte[]) arg2, 1, pc);
+ }
+ }
+
+ public static native void handleLibraryLoad();
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDivHooks.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDivHooks.java
new file mode 100644
index 00000000..c4991eb5
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDivHooks.java
@@ -0,0 +1,47 @@
+// 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.runtime;
+
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+
+@SuppressWarnings("unused")
+final public class TraceDivHooks {
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer",
+ targetMethod = "divideUnsigned", targetMethodDescriptor = "(II)I")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer",
+ targetMethod = "remainderUnsigned", targetMethodDescriptor = "(II)I")
+ public static void
+ intUnsignedDivide(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ // Since the arguments are to be treated as unsigned integers we need a long to fit the
+ // divisor.
+ TraceDataFlowNativeCallbacks.traceDivLong(Integer.toUnsignedLong((int) arguments[1]), hookId);
+ }
+
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long",
+ targetMethod = "divideUnsigned", targetMethodDescriptor = "(JJ)J")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long",
+ targetMethod = "remainderUnsigned", targetMethodDescriptor = "(JJ)J")
+ public static void
+ longUnsignedDivide(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ long divisor = (long) arguments[1];
+ // Run the callback only if the divisor, which is regarded as an unsigned long, fits in a
+ // signed long, i.e., does not have the sign bit set.
+ if (divisor > 0) {
+ TraceDataFlowNativeCallbacks.traceDivLong(divisor, hookId);
+ }
+ }
+}
diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceIndirHooks.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceIndirHooks.java
new file mode 100644
index 00000000..897ede6c
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceIndirHooks.java
@@ -0,0 +1,35 @@
+// 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.runtime;
+
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+import java.lang.reflect.Executable;
+
+@SuppressWarnings("unused")
+final public class TraceIndirHooks {
+ // The reflection hook is of type AFTER as it should only report calls that did not fail because
+ // of incorrect arguments passed.
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.reflect.Method", targetMethod = "invoke")
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.reflect.Constructor",
+ targetMethod = "newInstance")
+ public static void
+ methodInvoke(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) {
+ TraceDataFlowNativeCallbacks.traceReflectiveCall((Executable) thisObject, hookId);
+ }
+}
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
new file mode 100644
index 00000000..5e301efc
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel
@@ -0,0 +1,10 @@
+load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library")
+
+kt_jvm_library(
+ name = "utils",
+ srcs = [
+ "ClassNameGlobber.kt",
+ "Utils.kt",
+ ],
+ visibility = ["//visibility:public"],
+)
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
new file mode 100644
index 00000000..1f09afe3
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt
@@ -0,0 +1,102 @@
+// 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.lang.IllegalArgumentException
+
+private val BASE_INCLUDED_CLASS_NAME_GLOBS = listOf(
+ "**", // everything
+)
+
+private val BASE_EXCLUDED_CLASS_NAME_GLOBS = listOf(
+ "\\[**", // 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.**",
+)
+
+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)
+ .map(::SimpleGlobMatcher)
+
+ // If no include globs are provided, additionally exclude stdlib classes as well as our own classes.
+ private val excludeMatchers = (if (includes.isEmpty()) BASE_EXCLUDED_CLASS_NAME_GLOBS + excludes else excludes)
+ .map(::SimpleGlobMatcher)
+
+ fun includes(className: String): Boolean {
+ return includeMatchers.any { it.matches(className) } && excludeMatchers.none { it.matches(className) }
+ }
+}
+
+class SimpleGlobMatcher(val glob: String) {
+ private enum class Type {
+ // foo.bar (matches foo.bar only)
+ FULL_MATCH,
+ // foo.** (matches foo.bar and foo.bar.baz)
+ PATH_WILDCARD_SUFFIX,
+ // foo.* (matches foo.bar, but not foo.bar.baz)
+ SEGMENT_WILDCARD_SUFFIX,
+ }
+
+ private val type: Type
+ private val prefix: String
+
+ init {
+ // Remain compatible with globs such as "\\[" that use escaping.
+ val pattern = glob.replace("\\", "")
+ when {
+ !pattern.contains('*') -> {
+ type = Type.FULL_MATCH
+ prefix = pattern
+ }
+ // Ends with "**" and contains no other '*'.
+ pattern.endsWith("**") && pattern.indexOf('*') == pattern.length - 2 -> {
+ type = Type.PATH_WILDCARD_SUFFIX
+ prefix = pattern.removeSuffix("**")
+ }
+ // Ends with "*" and contains no other '*'.
+ pattern.endsWith('*') && pattern.indexOf('*') == pattern.length - 1 -> {
+ type = Type.SEGMENT_WILDCARD_SUFFIX
+ prefix = pattern.removeSuffix("*")
+ }
+ else -> throw IllegalArgumentException(
+ "Unsupported glob pattern (only foo.bar, foo.* and foo.** are supported): $pattern"
+ )
+ }
+ }
+
+ /**
+ * Checks whether [maybeInternalClassName], which may be internal (foo/bar) or not (foo.bar), matches [glob].
+ */
+ fun matches(maybeInternalClassName: String): Boolean {
+ val className = maybeInternalClassName.replace('/', '.')
+ return when (type) {
+ Type.FULL_MATCH -> className == prefix
+ Type.PATH_WILDCARD_SUFFIX -> className.startsWith(prefix)
+ Type.SEGMENT_WILDCARD_SUFFIX -> {
+ // className starts with prefix and contains no further '.'.
+ className.startsWith(prefix) &&
+ className.indexOf('.', startIndex = prefix.length) == -1
+ }
+ }
+ }
+}
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
new file mode 100644
index 00000000..af8cce9b
--- /dev/null
+++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt
@@ -0,0 +1,82 @@
+// 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("Utils")
+
+package com.code_intelligence.jazzer.utils
+
+import java.lang.reflect.Executable
+import java.lang.reflect.Method
+
+val Class<*>.descriptor: String
+ get() = when {
+ isPrimitive -> {
+ when (this) {
+ Boolean::class.javaPrimitiveType -> "Z"
+ Byte::class.javaPrimitiveType -> "B"
+ Char::class.javaPrimitiveType -> "C"
+ Short::class.javaPrimitiveType -> "S"
+ Int::class.javaPrimitiveType -> "I"
+ Long::class.javaPrimitiveType -> "J"
+ Float::class.javaPrimitiveType -> "F"
+ Double::class.javaPrimitiveType -> "D"
+ java.lang.Void::class.javaPrimitiveType -> "V"
+ else -> throw IllegalStateException("Unknown primitive type: $name")
+ }
+ }
+ isArray -> "[${componentType.descriptor}"
+ java.lang.Object::class.java.isAssignableFrom(this) -> "L${name.replace('.', '/')};"
+ else -> throw IllegalArgumentException("Unknown class type: $name")
+ }
+
+val Class<*>.readableDescriptor: String
+ get() = when {
+ isPrimitive -> {
+ when (this) {
+ Boolean::class.javaPrimitiveType -> "boolean"
+ Byte::class.javaPrimitiveType -> "byte"
+ Char::class.javaPrimitiveType -> "char"
+ Short::class.javaPrimitiveType -> "short"
+ Int::class.javaPrimitiveType -> "int"
+ Long::class.javaPrimitiveType -> "long"
+ Float::class.javaPrimitiveType -> "float"
+ Double::class.javaPrimitiveType -> "double"
+ java.lang.Void::class.javaPrimitiveType -> "void"
+ else -> throw IllegalStateException("Unknown primitive type: $name")
+ }
+ }
+ isArray -> "${componentType.readableDescriptor}[]"
+ java.lang.Object::class.java.isAssignableFrom(this) -> name
+ else -> throw IllegalArgumentException("Unknown class type: $name")
+ }
+
+val Executable.descriptor: String
+ get() = parameterTypes.joinToString(separator = "", prefix = "(", postfix = ")") { parameterType ->
+ parameterType.descriptor
+ } + if (this is Method) returnType.descriptor else "V"
+
+// This does not include the return type as the parameter descriptors already uniquely identify the executable.
+val Executable.readableDescriptor: String
+ get() = parameterTypes.joinToString(separator = ",", prefix = "(", postfix = ")") { parameterType ->
+ parameterType.readableDescriptor
+ }
+
+fun simpleFastHash(vararg strings: String): Int {
+ var hash = 0
+ for (string in strings) {
+ for (c in string) {
+ hash = hash * 11 + c.code
+ }
+ }
+ return hash
+}
diff --git a/agent/src/main/native/com/code_intelligence/jazzer/replay/BUILD.bazel b/agent/src/main/native/com/code_intelligence/jazzer/replay/BUILD.bazel
new file mode 100644
index 00000000..6b75fb8b
--- /dev/null
+++ b/agent/src/main/native/com/code_intelligence/jazzer/replay/BUILD.bazel
@@ -0,0 +1,13 @@
+load("@fmeum_rules_jni//jni:defs.bzl", "cc_jni_library")
+
+cc_jni_library(
+ name = "replay",
+ srcs = [
+ "com_code_intelligence_jazzer_replay_Replayer.cpp",
+ ],
+ visibility = ["//agent/src/main/java/com/code_intelligence/jazzer/replay:__pkg__"],
+ deps = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/replay:replay.hdrs",
+ "//driver:fuzzed_data_provider",
+ ],
+)
diff --git a/agent/src/main/native/com/code_intelligence/jazzer/replay/com_code_intelligence_jazzer_replay_Replayer.cpp b/agent/src/main/native/com/code_intelligence/jazzer/replay/com_code_intelligence_jazzer_replay_Replayer.cpp
new file mode 100644
index 00000000..c4bdfcfb
--- /dev/null
+++ b/agent/src/main/native/com/code_intelligence/jazzer/replay/com_code_intelligence_jazzer_replay_Replayer.cpp
@@ -0,0 +1,48 @@
+// 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.
+
+#include "com_code_intelligence_jazzer_replay_Replayer.h"
+
+#include <jni.h>
+
+#include "driver/fuzzed_data_provider.h"
+
+namespace {
+uint8_t *data = nullptr;
+}
+
+void Java_com_code_1intelligence_jazzer_replay_Replayer_feedFuzzedDataProvider(
+ JNIEnv *env, jclass, jbyteArray input) {
+ if (data == nullptr) {
+ jazzer::SetUpFuzzedDataProvider(*env);
+ } else {
+ delete[] data;
+ }
+
+ std::size_t size = env->GetArrayLength(input);
+ if (env->ExceptionCheck()) {
+ env->ExceptionDescribe();
+ env->FatalError("Failed to get length of input");
+ }
+ data = static_cast<uint8_t *>(operator new(size));
+ if (data == nullptr) {
+ env->FatalError("Failed to allocate memory for a copy of the input");
+ }
+ env->GetByteArrayRegion(input, 0, size, reinterpret_cast<jbyte *>(data));
+ if (env->ExceptionCheck()) {
+ env->ExceptionDescribe();
+ env->FatalError("Failed to copy input");
+ }
+ jazzer::FeedFuzzedDataProvider(data, size);
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java b/agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java
new file mode 100644
index 00000000..66a85db6
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java
@@ -0,0 +1,107 @@
+// 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.api;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.Test;
+
+public class AutofuzzTest {
+ public interface UnimplementedInterface {}
+
+ public interface ImplementedInterface {}
+ public static class ImplementingClass implements ImplementedInterface {}
+
+ private static boolean implIsNotNull(ImplementedInterface impl) {
+ return impl != null;
+ }
+
+ private static boolean implIsNotNull(UnimplementedInterface impl) {
+ return impl != null;
+ }
+
+ private static void checkAllTheArguments(
+ String arg1, int arg2, byte arg3, ImplementedInterface arg4) {
+ if (!arg1.equals("foobar") || arg2 != 42 || arg3 != 5 || arg4 == null) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ @Test
+ public void testConsume() {
+ FuzzedDataProvider data = CannedFuzzedDataProvider.create(
+ Arrays.asList((byte) 1 /* do not return null */, 0 /* first class on the classpath */,
+ (byte) 1 /* do not return null */, 0 /* first constructor */));
+ ImplementedInterface result = Jazzer.consume(data, ImplementedInterface.class);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testConsumeFailsWithoutException() {
+ FuzzedDataProvider data = CannedFuzzedDataProvider.create(Collections.singletonList(
+ (byte) 1 /* do not return null without searching for implementing classes */));
+ assertNull(Jazzer.consume(data, UnimplementedInterface.class));
+ }
+
+ @Test
+ public void testAutofuzz() {
+ FuzzedDataProvider data = CannedFuzzedDataProvider.create(
+ Arrays.asList((byte) 1 /* do not return null */, 0 /* first class on the classpath */,
+ (byte) 1 /* do not return null */, 0 /* first constructor */));
+ assertEquals(Boolean.TRUE,
+ Jazzer.autofuzz(data, (Function1<ImplementedInterface, ?>) AutofuzzTest::implIsNotNull));
+ }
+
+ @Test
+ public void testAutofuzzFailsWithException() {
+ FuzzedDataProvider data = CannedFuzzedDataProvider.create(
+ Collections.singletonList((byte) 1 /* do not return null */));
+ try {
+ Jazzer.autofuzz(data, (Function1<UnimplementedInterface, ?>) AutofuzzTest::implIsNotNull);
+ } catch (AutofuzzConstructionException e) {
+ // Pass.
+ return;
+ }
+ fail("should have thrown an AutofuzzConstructionException");
+ }
+
+ @Test
+ public void testAutofuzzConsumer() {
+ FuzzedDataProvider data = CannedFuzzedDataProvider.create(
+ Arrays.asList((byte) 1 /* do not return null */, 6 /* string length */, "foobar", 42,
+ (byte) 5, (byte) 1 /* do not return null */, 0 /* first class on the classpath */,
+ (byte) 1 /* do not return null */, 0 /* first constructor */));
+ Jazzer.autofuzz(data, AutofuzzTest::checkAllTheArguments);
+ }
+
+ @Test
+ public void testAutofuzzConsumerThrowsException() {
+ FuzzedDataProvider data =
+ CannedFuzzedDataProvider.create(Arrays.asList((byte) 1 /* do not return null */,
+ 6 /* string length */, "foobar", 42, (byte) 5, (byte) 0 /* *do* return null */));
+ try {
+ Jazzer.autofuzz(data, AutofuzzTest::checkAllTheArguments);
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ return;
+ }
+ fail("should have thrown an IllegalArgumentException");
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel b/agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel
new file mode 100644
index 00000000..9192ff77
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel
@@ -0,0 +1,21 @@
+java_test(
+ name = "AutofuzzTest",
+ size = "small",
+ srcs = [
+ "AutofuzzTest.java",
+ ],
+ env = {
+ # Also consider implementing classes from com.code_intelligence.jazzer.*.
+ "JAZZER_AUTOFUZZ_TESTING": "1",
+ },
+ test_class = "com.code_intelligence.jazzer.api.AutofuzzTest",
+ runtime_deps = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/autofuzz",
+ # Needed for JazzerInternal.
+ "//agent/src/main/java/com/code_intelligence/jazzer/runtime",
+ ],
+ deps = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/api",
+ "@maven//:junit_junit",
+ ],
+)
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel
new file mode 100644
index 00000000..f8448f01
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel
@@ -0,0 +1,69 @@
+java_test(
+ name = "MetaTest",
+ size = "small",
+ srcs = [
+ "MetaTest.java",
+ ],
+ test_class = "com.code_intelligence.jazzer.autofuzz.MetaTest",
+ deps = [
+ ":test_helpers",
+ "//agent/src/main/java/com/code_intelligence/jazzer/api",
+ "//agent/src/main/java/com/code_intelligence/jazzer/autofuzz",
+ "@maven//:com_mikesamuel_json_sanitizer",
+ "@maven//:junit_junit",
+ ],
+)
+
+java_test(
+ name = "InterfaceCreationTest",
+ size = "small",
+ srcs = [
+ "InterfaceCreationTest.java",
+ ],
+ env = {
+ # Also consider implementing classes from com.code_intelligence.jazzer.*.
+ "JAZZER_AUTOFUZZ_TESTING": "1",
+ },
+ test_class = "com.code_intelligence.jazzer.autofuzz.InterfaceCreationTest",
+ deps = [
+ ":test_helpers",
+ "@maven//:junit_junit",
+ ],
+)
+
+java_test(
+ name = "BuilderPatternTest",
+ size = "small",
+ srcs = [
+ "BuilderPatternTest.java",
+ ],
+ test_class = "com.code_intelligence.jazzer.autofuzz.BuilderPatternTest",
+ deps = [
+ ":test_helpers",
+ "@maven//:junit_junit",
+ ],
+)
+
+java_test(
+ name = "SettersTest",
+ size = "small",
+ srcs = [
+ "SettersTest.java",
+ ],
+ test_class = "com.code_intelligence.jazzer.autofuzz.SettersTest",
+ deps = [
+ ":test_helpers",
+ "//agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata:test_data",
+ "@maven//:junit_junit",
+ ],
+)
+
+java_library(
+ name = "test_helpers",
+ srcs = ["TestHelpers.java"],
+ deps = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/api",
+ "//agent/src/main/java/com/code_intelligence/jazzer/autofuzz",
+ "@maven//:junit_junit",
+ ],
+)
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/BuilderPatternTest.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/BuilderPatternTest.java
new file mode 100644
index 00000000..a602d712
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/BuilderPatternTest.java
@@ -0,0 +1,103 @@
+// 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.autofuzz;
+
+import static com.code_intelligence.jazzer.autofuzz.TestHelpers.consumeTestCase;
+
+import java.util.Arrays;
+import java.util.Objects;
+import org.junit.Test;
+
+class Employee {
+ private final String firstName;
+ private final String lastName;
+ private final String jobTitle;
+ private final int age;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ Employee hero = (Employee) o;
+ return age == hero.age && Objects.equals(firstName, hero.firstName)
+ && Objects.equals(lastName, hero.lastName) && Objects.equals(jobTitle, hero.jobTitle);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(firstName, lastName, jobTitle, age);
+ }
+
+ private Employee(Builder builder) {
+ this.jobTitle = builder.jobTitle;
+ this.firstName = builder.firstName;
+ this.lastName = builder.lastName;
+ this.age = builder.age;
+ }
+
+ public static class Builder {
+ private final String firstName;
+ private final String lastName;
+ private String jobTitle;
+ private int age;
+
+ public Builder(String firstName, String lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public Builder withAge(int age) {
+ this.age = age;
+ return this;
+ }
+
+ public Builder withJobTitle(String jobTitle) {
+ this.jobTitle = jobTitle;
+ return this;
+ }
+
+ public Employee build() {
+ return new Employee(this);
+ }
+ }
+}
+
+public class BuilderPatternTest {
+ @Test
+ public void testBuilderPattern() {
+ consumeTestCase(new Employee.Builder("foo", "bar").withAge(20).withJobTitle("baz").build(),
+ "new com.code_intelligence.jazzer.autofuzz.Employee.Builder(\"foo\", \"bar\").withAge(20).withJobTitle(\"baz\").build()",
+ Arrays.asList((byte) 1, // do not return null
+ 0, // Select the first Builder
+ 2, // Select two Builder methods returning a builder object (fluent design)
+ 0, // Select the first build method
+ 0, // pick the first remaining builder method (withAge)
+ 0, // pick the first remaining builder method (withJobTitle)
+ 0, // pick the first build method
+ (byte) 1, // do not return null
+ 6, // remaining bytes
+ "foo", // firstName
+ (byte) 1, // do not return null
+ 6, // remaining bytes
+ "bar", // lastName
+ 20, // age
+ (byte) 1, // do not return null
+ 6, // remaining bytes
+ "baz" // jobTitle
+ ));
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/InterfaceCreationTest.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/InterfaceCreationTest.java
new file mode 100644
index 00000000..4d85ca6c
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/InterfaceCreationTest.java
@@ -0,0 +1,111 @@
+// 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.autofuzz;
+
+import static com.code_intelligence.jazzer.autofuzz.TestHelpers.consumeTestCase;
+
+import java.util.Arrays;
+import java.util.Objects;
+import org.junit.Test;
+
+interface InterfaceA {
+ void foo();
+
+ void bar();
+}
+
+abstract class ClassA1 implements InterfaceA {
+ @Override
+ public void foo() {}
+}
+
+class ClassB1 extends ClassA1 {
+ int n;
+
+ public ClassB1(int _n) {
+ n = _n;
+ }
+
+ @Override
+ public void bar() {}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ ClassB1 classB1 = (ClassB1) o;
+ return n == classB1.n;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(n);
+ }
+}
+
+class ClassB2 implements InterfaceA {
+ String s;
+
+ public ClassB2(String _s) {
+ s = _s;
+ }
+
+ @Override
+ public void foo() {}
+
+ @Override
+ public void bar() {}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ ClassB2 classB2 = (ClassB2) o;
+ return Objects.equals(s, classB2.s);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(s);
+ }
+}
+
+public class InterfaceCreationTest {
+ @Test
+ public void testConsumeInterface() {
+ consumeTestCase(InterfaceA.class, new ClassB1(5),
+ "(com.code_intelligence.jazzer.autofuzz.InterfaceA) new com.code_intelligence.jazzer.autofuzz.ClassB1(5)",
+ Arrays.asList((byte) 1, // do not return null
+ 0, // pick ClassB1
+ (byte) 1, // do not return null
+ 0, // pick first constructor
+ 5 // arg for ClassB1 constructor
+ ));
+ consumeTestCase(InterfaceA.class, new ClassB2("test"),
+ "(com.code_intelligence.jazzer.autofuzz.InterfaceA) new com.code_intelligence.jazzer.autofuzz.ClassB2(\"test\")",
+ Arrays.asList((byte) 1, // do not return null
+ 1, // pick ClassB2
+ (byte) 1, // do not return null
+ 0, // pick first constructor
+ (byte) 1, // do not return null
+ 8, // remaining bytes
+ "test" // arg for ClassB2 constructor
+ ));
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java
new file mode 100644
index 00000000..0615e9ae
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java
@@ -0,0 +1,147 @@
+// 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.autofuzz;
+
+import static com.code_intelligence.jazzer.autofuzz.TestHelpers.autofuzzTestCase;
+import static com.code_intelligence.jazzer.autofuzz.TestHelpers.consumeTestCase;
+import static org.junit.Assert.assertEquals;
+
+import com.code_intelligence.jazzer.api.CannedFuzzedDataProvider;
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.google.json.JsonSanitizer;
+import java.io.ByteArrayInputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.Test;
+
+public class MetaTest {
+ public static boolean isFive(int arg) {
+ return arg == 5;
+ }
+
+ public static boolean intEquals(int arg1, int arg2) {
+ return arg1 == arg2;
+ }
+
+ public enum TestEnum {
+ FOO,
+ BAR,
+ BAZ,
+ }
+
+ @Test
+ public void testConsume() {
+ consumeTestCase(5, "5", Collections.singletonList(5));
+ consumeTestCase((short) 5, "(short) 5", Collections.singletonList((short) 5));
+ consumeTestCase(5L, "5L", Collections.singletonList(5L));
+ consumeTestCase(5.0F, "5.0F", Collections.singletonList(5.0F));
+ consumeTestCase('\n', "'\\\\n'", Collections.singletonList('\n'));
+ consumeTestCase('\'', "'\\\\''", Collections.singletonList('\''));
+ consumeTestCase('\\', "'\\\\'", Collections.singletonList('\\'));
+
+ String testString = "foo\n\t\\\"bar";
+ // The expected string is obtained from testString by escaping, wrapping into quotes and
+ // escaping again.
+ consumeTestCase(testString, "\"foo\\\\n\\\\t\\\\\\\\\"bar\"",
+ Arrays.asList((byte) 1, // do not return null
+ testString.length(), testString));
+
+ consumeTestCase(null, "null", Collections.singletonList((byte) 0));
+
+ boolean[] testBooleans = new boolean[] {true, false, true};
+ consumeTestCase(testBooleans, "new boolean[]{true, false, true}",
+ Arrays.asList((byte) 1, // do not return null for the array
+ 2 * 3, testBooleans));
+
+ char[] testChars = new char[] {'a', '\n', '\''};
+ consumeTestCase(testChars, "new char[]{'a', '\\\\n', '\\\\''}",
+ Arrays.asList((byte) 1, // do not return null for the array
+ 2 * 3 * Character.BYTES + Character.BYTES, testChars[0], 2 * 3 * Character.BYTES,
+ 2 * 3 * Character.BYTES, // remaining bytes, 2 times what is needed for 3 chars
+ testChars[1], testChars[2]));
+
+ char[] testNoChars = new char[] {};
+ consumeTestCase(testNoChars, "new char[]{}",
+ Arrays.asList((byte) 1, // do not return null for the array
+ 0, 'a', 0, 0));
+
+ short[] testShorts = new short[] {(short) 1, (short) 2, (short) 3};
+ consumeTestCase(testShorts, "new short[]{(short) 1, (short) 2, (short) 3}",
+ Arrays.asList((byte) 1, // do not return null for the array
+ 2 * 3 * Short.BYTES, // remaining bytes
+ testShorts));
+
+ long[] testLongs = new long[] {1L, 2L, 3L};
+ consumeTestCase(testLongs, "new long[]{1L, 2L, 3L}",
+ Arrays.asList((byte) 1, // do not return null for the array
+ 2 * 3 * Long.BYTES, // remaining bytes
+ testLongs));
+
+ consumeTestCase(new String[] {"foo", "bar", "foo\nbar"},
+ "new java.lang.String[]{\"foo\", \"bar\", \"foo\\\\nbar\"}",
+ Arrays.asList((byte) 1, // do not return null for the array
+ 32, // remaining bytes
+ (byte) 1, // do not return null for the string
+ 31, // remaining bytes
+ "foo",
+ 28, // remaining bytes
+ 28, // array length
+ (byte) 1, // do not return null for the string
+ 27, // remaining bytes
+ "bar",
+ (byte) 1, // do not return null for the string
+ 23, // remaining bytes
+ "foo\nbar"));
+
+ byte[] testInputStreamBytes = new byte[] {(byte) 1, (byte) 2, (byte) 3};
+ consumeTestCase(new ByteArrayInputStream(testInputStreamBytes),
+ "new java.io.ByteArrayInputStream(new byte[]{(byte) 1, (byte) 2, (byte) 3})",
+ Arrays.asList((byte) 1, // do not return null for the InputStream
+ 2 * 3, // remaining bytes (twice the desired length)
+ testInputStreamBytes));
+
+ consumeTestCase(TestEnum.BAR,
+ String.format("%s.%s", TestEnum.class.getName(), TestEnum.BAR.name()),
+ Arrays.asList((byte) 1, // do not return null for the enum value
+ 1 /* second value */
+ ));
+
+ consumeTestCase(YourAverageJavaClass.class,
+ "com.code_intelligence.jazzer.autofuzz.YourAverageJavaClass.class",
+ Collections.singletonList((byte) 1));
+ }
+
+ @Test
+ public void testAutofuzz() throws NoSuchMethodException {
+ autofuzzTestCase(true, "com.code_intelligence.jazzer.autofuzz.MetaTest.isFive(5)",
+ MetaTest.class.getMethod("isFive", int.class), Collections.singletonList(5));
+ autofuzzTestCase(false, "com.code_intelligence.jazzer.autofuzz.MetaTest.intEquals(5, 4)",
+ MetaTest.class.getMethod("intEquals", int.class, int.class), Arrays.asList(5, 4));
+ autofuzzTestCase("foobar", "\"foo\".concat(\"bar\")",
+ String.class.getMethod("concat", String.class),
+ Arrays.asList((byte) 1, 6, "foo", (byte) 1, 6, "bar"));
+ autofuzzTestCase("jazzer", "new java.lang.String(\"jazzer\")",
+ String.class.getConstructor(String.class), Arrays.asList((byte) 1, 12, "jazzer"));
+ autofuzzTestCase("\"jazzer\"", "com.google.json.JsonSanitizer.sanitize(\"jazzer\")",
+ JsonSanitizer.class.getMethod("sanitize", String.class),
+ Arrays.asList((byte) 1, 12, "jazzer"));
+
+ FuzzedDataProvider data =
+ CannedFuzzedDataProvider.create(Arrays.asList((byte) 1, // do not return null
+ 8, // remainingBytes
+ "buzz"));
+ assertEquals("fizzbuzz", Meta.autofuzz(data, "fizz" ::concat));
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/SettersTest.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/SettersTest.java
new file mode 100644
index 00000000..7c869531
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/SettersTest.java
@@ -0,0 +1,43 @@
+// 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.autofuzz;
+
+import static com.code_intelligence.jazzer.autofuzz.TestHelpers.consumeTestCase;
+
+import com.code_intelligence.jazzer.autofuzz.testdata.EmployeeWithSetters;
+import java.util.Arrays;
+import org.junit.Test;
+
+public class SettersTest {
+ @Test
+ public void testEmptyConstructorWithSetters() {
+ EmployeeWithSetters employee = new EmployeeWithSetters();
+ employee.setFirstName("foo");
+ employee.setAge(26);
+
+ consumeTestCase(employee,
+ "((java.util.function.Supplier<com.code_intelligence.jazzer.autofuzz.testdata.EmployeeWithSetters>) (() -> {com.code_intelligence.jazzer.autofuzz.testdata.EmployeeWithSetters autofuzzVariable0 = new com.code_intelligence.jazzer.autofuzz.testdata.EmployeeWithSetters(); autofuzzVariable0.setFirstName(\"foo\"); autofuzzVariable0.setAge(26); return autofuzzVariable0;})).get()",
+ Arrays.asList((byte) 1, // do not return null for EmployeeWithSetters
+ 0, // pick first constructor
+ 2, // pick two setters
+ 1, // pick second setter
+ 0, // pick first setter
+ (byte) 1, // do not return null for String
+ 6, // remaining bytes
+ "foo", // setFirstName
+ 26 // setAge
+ ));
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.java
new file mode 100644
index 00000000..52f19a74
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.java
@@ -0,0 +1,85 @@
+// 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.autofuzz;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.code_intelligence.jazzer.api.CannedFuzzedDataProvider;
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import java.io.ByteArrayInputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Method;
+import java.util.List;
+
+class TestHelpers {
+ static void assertGeneralEquals(Object expected, Object actual) {
+ Class<?> type = expected != null ? expected.getClass() : Object.class;
+ if (type.isArray()) {
+ if (type.getComponentType() == boolean.class) {
+ assertArrayEquals((boolean[]) expected, (boolean[]) actual);
+ } else if (type.getComponentType() == char.class) {
+ assertArrayEquals((char[]) expected, (char[]) actual);
+ } else if (type.getComponentType() == short.class) {
+ assertArrayEquals((short[]) expected, (short[]) actual);
+ } else if (type.getComponentType() == long.class) {
+ assertArrayEquals((long[]) expected, (long[]) actual);
+ } else {
+ assertArrayEquals((Object[]) expected, (Object[]) actual);
+ }
+ } else if (type == ByteArrayInputStream.class) {
+ ByteArrayInputStream expectedStream = (ByteArrayInputStream) expected;
+ ByteArrayInputStream actualStream = (ByteArrayInputStream) actual;
+ assertArrayEquals(readAllBytes(expectedStream), readAllBytes(actualStream));
+ } else {
+ assertEquals(expected, actual);
+ }
+ }
+
+ static void consumeTestCase(
+ Object expectedResult, String expectedResultString, List<Object> cannedData) {
+ Class<?> type = expectedResult != null ? expectedResult.getClass() : Object.class;
+ consumeTestCase(type, expectedResult, expectedResultString, cannedData);
+ }
+
+ static void consumeTestCase(
+ Class<?> type, Object expectedResult, String expectedResultString, List<Object> cannedData) {
+ assertTrue(expectedResult == null || type.isAssignableFrom(expectedResult.getClass()));
+ AutofuzzCodegenVisitor visitor = new AutofuzzCodegenVisitor();
+ FuzzedDataProvider data = CannedFuzzedDataProvider.create(cannedData);
+ assertGeneralEquals(expectedResult, Meta.consume(data, type, visitor));
+ assertEquals(expectedResultString, visitor.generate());
+ }
+
+ static void autofuzzTestCase(Object expectedResult, String expectedResultString, Executable func,
+ List<Object> cannedData) {
+ AutofuzzCodegenVisitor visitor = new AutofuzzCodegenVisitor();
+ FuzzedDataProvider data = CannedFuzzedDataProvider.create(cannedData);
+ if (func instanceof Method) {
+ assertGeneralEquals(expectedResult, Meta.autofuzz(data, (Method) func, visitor));
+ } else {
+ assertGeneralEquals(expectedResult, Meta.autofuzz(data, (Constructor<?>) func, visitor));
+ }
+ assertEquals(expectedResultString, visitor.generate());
+ }
+
+ private static byte[] readAllBytes(ByteArrayInputStream in) {
+ byte[] result = new byte[in.available()];
+ in.read(result, 0, in.available());
+ return result;
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata/BUILD.bazel b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata/BUILD.bazel
new file mode 100644
index 00000000..c2c68803
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata/BUILD.bazel
@@ -0,0 +1,5 @@
+java_library(
+ name = "test_data",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+)
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata/EmployeeWithSetters.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata/EmployeeWithSetters.java
new file mode 100644
index 00000000..2c76a61f
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata/EmployeeWithSetters.java
@@ -0,0 +1,56 @@
+// 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.autofuzz.testdata;
+
+import java.util.Objects;
+
+public class EmployeeWithSetters {
+ private String firstName;
+ private String lastName;
+ private String jobTitle;
+ private int age;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ EmployeeWithSetters hero = (EmployeeWithSetters) o;
+ return age == hero.age && Objects.equals(firstName, hero.firstName)
+ && Objects.equals(lastName, hero.lastName) && Objects.equals(jobTitle, hero.jobTitle);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(firstName, lastName, jobTitle, age);
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public void setJobTitle(String jobTitle) {
+ this.jobTitle = jobTitle;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooks.java
new file mode 100644
index 00000000..f8d6782c
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooks.java
@@ -0,0 +1,87 @@
+// 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.instrumentor;
+
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+
+public class AfterHooks {
+ static AfterHooksTargetContract instance;
+
+ @MethodHook(type = HookType.AFTER,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.AfterHooksTarget",
+ targetMethod = "func1")
+ public static void
+ patchFunc1(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) {
+ instance = (AfterHooksTargetContract) thisObject;
+ ((AfterHooksTargetContract) thisObject).registerHasFunc1BeenCalled();
+ }
+
+ @MethodHook(type = HookType.AFTER,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.AfterHooksTarget",
+ targetMethod = "registerTimesCalled", targetMethodDescriptor = "()V")
+ public static void
+ patchRegisterTimesCalled(MethodHandle method, Object thisObject, Object[] arguments, int hookId,
+ Object returnValue) throws Throwable {
+ // Invoke registerTimesCalled() again to pass the test.
+ method.invoke();
+ }
+
+ @MethodHook(type = HookType.AFTER,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.AfterHooksTarget",
+ targetMethod = "getFirstSecret", targetMethodDescriptor = "()Ljava/lang/String;")
+ public static void
+ patchGetFirstSecret(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, String returnValue) {
+ // Use the returned secret to pass the test.
+ ((AfterHooksTargetContract) thisObject).verifyFirstSecret(returnValue);
+ }
+
+ @MethodHook(type = HookType.AFTER,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.AfterHooksTarget",
+ targetMethod = "getSecondSecret")
+ public static void
+ patchGetSecondSecret(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) {
+ // Use the returned secret to pass the test.
+ ((AfterHooksTargetContract) thisObject).verifySecondSecret((String) returnValue);
+ }
+
+ // Verify the interaction of a BEFORE and an AFTER hook. The BEFORE hook modifies the argument of
+ // the StringBuilder constructor.
+ @MethodHook(
+ type = HookType.BEFORE, targetClassName = "java.lang.StringBuilder", targetMethod = "<init>")
+ public static void
+ patchStringBuilderBeforeInit(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ arguments[0] = "hunter3";
+ }
+
+ @MethodHook(
+ type = HookType.AFTER, targetClassName = "java.lang.StringBuilder", targetMethod = "<init>")
+ public static void
+ patchStringBuilderInit(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) {
+ String secret = ((StringBuilder) thisObject).toString();
+ // Verify that the argument passed to this AFTER hook agrees with the argument passed to the
+ // StringBuilder constructor, which has been modified by the BEFORE hook.
+ if (secret.equals(arguments[0])) {
+ // Verify that the argument has been modified to the correct value "hunter3".
+ instance.verifyThirdSecret(secret);
+ }
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt
new file mode 100644
index 00000000..53efd200
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt
@@ -0,0 +1,63 @@
+// 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.instrumentor
+
+import org.junit.Test
+import java.io.File
+
+private fun applyAfterHooks(bytecode: ByteArray): ByteArray {
+ return HookInstrumentor(loadHooks(AfterHooks::class.java), false).instrument(bytecode)
+}
+
+private fun getOriginalAfterHooksTargetInstance(): AfterHooksTargetContract {
+ return AfterHooksTarget()
+}
+
+private fun getNoHooksAfterHooksTargetInstance(): AfterHooksTargetContract {
+ val originalBytecode = classToBytecode(AfterHooksTarget::class.java)
+ // Let the bytecode pass through the hooking logic, but don't apply any hooks.
+ val patchedBytecode = HookInstrumentor(emptyList(), false).instrument(originalBytecode)
+ val patchedClass = bytecodeToClass(AfterHooksTarget::class.java.name, patchedBytecode)
+ return patchedClass.getDeclaredConstructor().newInstance() as AfterHooksTargetContract
+}
+
+private fun getPatchedAfterHooksTargetInstance(): AfterHooksTargetContract {
+ val originalBytecode = classToBytecode(AfterHooksTarget::class.java)
+ val patchedBytecode = applyAfterHooks(originalBytecode)
+ // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection.
+ val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR")
+ File("$outDir/${AfterHooksTarget::class.java.simpleName}.class").writeBytes(originalBytecode)
+ File("$outDir/${AfterHooksTarget::class.java.simpleName}.patched.class").writeBytes(patchedBytecode)
+ val patchedClass = bytecodeToClass(AfterHooksTarget::class.java.name, patchedBytecode)
+ return patchedClass.getDeclaredConstructor().newInstance() as AfterHooksTargetContract
+}
+
+class AfterHookTest {
+
+ @Test
+ fun testAfterHooksOriginal() {
+ assertSelfCheck(getOriginalAfterHooksTargetInstance(), false)
+ }
+
+ @Test
+ fun testAfterHooksNoHooks() {
+ assertSelfCheck(getNoHooksAfterHooksTargetInstance(), false)
+ }
+
+ @Test
+ fun testAfterHooksPatched() {
+ assertSelfCheck(getPatchedAfterHooksTargetInstance(), true)
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTarget.java
new file mode 100644
index 00000000..a47b03a5
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTarget.java
@@ -0,0 +1,85 @@
+// 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.instrumentor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+// selfCheck() only passes with the hooks in AfterHooks.java applied.
+public class AfterHooksTarget implements AfterHooksTargetContract {
+ static Map<String, Boolean> results = new HashMap<>();
+ static int timesCalled = 0;
+ Boolean func1Called = false;
+
+ public static void registerTimesCalled() {
+ timesCalled++;
+ results.put("hasBeenCalledTwice", timesCalled == 2);
+ }
+
+ public Map<String, Boolean> selfCheck() {
+ results = new HashMap<>();
+
+ if (results.isEmpty()) {
+ registerHasFunc1BeenCalled();
+ func1();
+ }
+
+ timesCalled = 0;
+ registerTimesCalled();
+
+ verifyFirstSecret("not_secret");
+ getFirstSecret();
+
+ verifySecondSecret("not_secret_at_all");
+ getSecondSecret();
+
+ verifyThirdSecret("not_the_secret");
+ new StringBuilder("not_hunter3");
+
+ return results;
+ }
+
+ public void func1() {
+ func1Called = true;
+ }
+
+ public void registerHasFunc1BeenCalled() {
+ results.put("hasFunc1BeenCalled", func1Called);
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ String getFirstSecret() {
+ return "hunter2";
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ public void verifyFirstSecret(String secret) {
+ results.put("verifyFirstSecret", secret.equals("hunter2"));
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ String getSecondSecret() {
+ return "hunter2!";
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ public void verifySecondSecret(String secret) {
+ results.put("verifySecondSecret", secret.equals("hunter2!"));
+ }
+
+ public void verifyThirdSecret(String secret) {
+ results.put("verifyThirdSecret", secret.equals("hunter3"));
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTargetContract.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTargetContract.java
new file mode 100644
index 00000000..cb12b148
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTargetContract.java
@@ -0,0 +1,29 @@
+// 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.instrumentor;
+
+/**
+ * Helper interface used to call methods on instances of AfterHooksTarget classes loaded via
+ * different class loaders.
+ */
+public interface AfterHooksTargetContract extends DynamicTestContract {
+ void registerHasFunc1BeenCalled();
+
+ void verifyFirstSecret(String secret);
+
+ void verifySecondSecret(String secret);
+
+ void verifyThirdSecret(String secret);
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel
new file mode 100644
index 00000000..472d2b98
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel
@@ -0,0 +1,147 @@
+load("//bazel:kotlin.bzl", "wrapped_kt_jvm_test")
+load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library")
+
+kt_jvm_library(
+ name = "patch_test_utils",
+ srcs = [
+ "DynamicTestContract.java",
+ "PatchTestUtils.kt",
+ ],
+)
+
+wrapped_kt_jvm_test(
+ name = "trace_data_flow_instrumentation_test",
+ size = "small",
+ srcs = [
+ "MockTraceDataFlowCallbacks.java",
+ "TraceDataFlowInstrumentationTarget.java",
+ "TraceDataFlowInstrumentationTest.kt",
+ ],
+ associates = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor",
+ ],
+ test_class = "com.code_intelligence.jazzer.instrumentor.TraceDataFlowInstrumentationTest",
+ deps = [
+ ":patch_test_utils",
+ "@com_github_jetbrains_kotlin//:kotlin-test",
+ "@maven//:junit_junit",
+ ],
+)
+
+wrapped_kt_jvm_test(
+ name = "coverage_instrumentation_test",
+ size = "small",
+ srcs = [
+ "CoverageInstrumentationSpecialCasesTarget.java",
+ "CoverageInstrumentationTarget.java",
+ "CoverageInstrumentationTest.kt",
+ "MockCoverageMap.java",
+ ],
+ associates = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor",
+ ],
+ test_class = "com.code_intelligence.jazzer.instrumentor.CoverageInstrumentationTest",
+ deps = [
+ ":patch_test_utils",
+ "@com_github_jetbrains_kotlin//:kotlin-test",
+ "@maven//:junit_junit",
+ ],
+)
+
+wrapped_kt_jvm_test(
+ name = "descriptor_utils_test",
+ size = "small",
+ srcs = [
+ "DescriptorUtilsTest.kt",
+ ],
+ associates = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor",
+ ],
+ test_class = "com.code_intelligence.jazzer.instrumentor.DescriptorUtilsTest",
+ deps = [
+ "@com_github_jetbrains_kotlin//:kotlin-test",
+ "@maven//:junit_junit",
+ ],
+)
+
+wrapped_kt_jvm_test(
+ name = "hook_validation_test",
+ size = "small",
+ srcs = [
+ "HookValidationTest.kt",
+ "InvalidHookMocks.java",
+ "ValidHookMocks.java",
+ ],
+ associates = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor",
+ ],
+ test_class = "com.code_intelligence.jazzer.instrumentor.HookValidationTest",
+ deps = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/api",
+ "@com_github_jetbrains_kotlin//:kotlin-test",
+ "@maven//:junit_junit",
+ ],
+)
+
+wrapped_kt_jvm_test(
+ name = "after_hooks_patch_test",
+ size = "small",
+ srcs = [
+ "AfterHooks.java",
+ "AfterHooksPatchTest.kt",
+ "AfterHooksTarget.java",
+ "AfterHooksTargetContract.java",
+ ],
+ associates = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor",
+ ],
+ test_class = "com.code_intelligence.jazzer.instrumentor.AfterHookTest",
+ deps = [
+ ":patch_test_utils",
+ "//agent/src/main/java/com/code_intelligence/jazzer/api",
+ "@com_github_jetbrains_kotlin//:kotlin-test",
+ "@maven//:junit_junit",
+ ],
+)
+
+wrapped_kt_jvm_test(
+ name = "before_hooks_patch_test",
+ size = "small",
+ srcs = [
+ "BeforeHooks.java",
+ "BeforeHooksPatchTest.kt",
+ "BeforeHooksTarget.java",
+ "BeforeHooksTargetContract.java",
+ ],
+ associates = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor",
+ ],
+ test_class = "com.code_intelligence.jazzer.instrumentor.BeforeHookTest",
+ deps = [
+ ":patch_test_utils",
+ "//agent/src/main/java/com/code_intelligence/jazzer/api",
+ "@com_github_jetbrains_kotlin//:kotlin-test",
+ "@maven//:junit_junit",
+ ],
+)
+
+wrapped_kt_jvm_test(
+ name = "replace_hooks_patch_test",
+ size = "small",
+ srcs = [
+ "ReplaceHooks.java",
+ "ReplaceHooksPatchTest.kt",
+ "ReplaceHooksTarget.java",
+ "ReplaceHooksTargetContract.java",
+ ],
+ associates = [
+ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor",
+ ],
+ test_class = "com.code_intelligence.jazzer.instrumentor.ReplaceHookTest",
+ deps = [
+ ":patch_test_utils",
+ "//agent/src/main/java/com/code_intelligence/jazzer/api",
+ "@com_github_jetbrains_kotlin//:kotlin-test",
+ "@maven//:junit_junit",
+ ],
+)
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooks.java
new file mode 100644
index 00000000..31577dad
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooks.java
@@ -0,0 +1,53 @@
+// 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.instrumentor;
+
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+
+public class BeforeHooks {
+ @MethodHook(type = HookType.BEFORE,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.BeforeHooksTarget",
+ targetMethod = "hasFunc1BeenCalled", targetMethodDescriptor = "()Z")
+ public static void
+ patchHasFunc1BeenCalled(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ ((BeforeHooksTargetContract) thisObject).func1();
+ }
+
+ @MethodHook(type = HookType.BEFORE,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.BeforeHooksTarget",
+ targetMethod = "getTimesCalled", targetMethodDescriptor = "()Ljava/lang/Integer;")
+ public static void
+ patchHasBeenCalled(MethodHandle method, Object thisObject, Object[] arguments, int hookId)
+ throws Throwable {
+ // Invoke static method getTimesCalled() again to pass the test.
+ method.invoke();
+ }
+
+ @MethodHook(type = HookType.BEFORE,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.BeforeHooksTarget",
+ targetMethod = "hasFuncWithArgsBeenCalled")
+ public static void
+ patchHasFuncWithArgsBeenCalled(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ if (arguments.length == 2 && arguments[0] instanceof Boolean
+ && arguments[1] instanceof String) {
+ // only if the arguments passed to the hook match the expected argument types and count invoke
+ // the method to pass the test
+ ((BeforeHooksTargetContract) thisObject).setFuncWithArgsCalled((Boolean) arguments[0]);
+ }
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt
new file mode 100644
index 00000000..31e9733c
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt
@@ -0,0 +1,63 @@
+// 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.instrumentor
+
+import org.junit.Test
+import java.io.File
+
+private fun applyBeforeHooks(bytecode: ByteArray): ByteArray {
+ return HookInstrumentor(loadHooks(BeforeHooks::class.java), false).instrument(bytecode)
+}
+
+private fun getOriginalBeforeHooksTargetInstance(): BeforeHooksTargetContract {
+ return BeforeHooksTarget()
+}
+
+private fun getNoHooksBeforeHooksTargetInstance(): BeforeHooksTargetContract {
+ val originalBytecode = classToBytecode(BeforeHooksTarget::class.java)
+ // Let the bytecode pass through the hooking logic, but don't apply any hooks.
+ val patchedBytecode = HookInstrumentor(emptyList(), false).instrument(originalBytecode)
+ val patchedClass = bytecodeToClass(BeforeHooksTarget::class.java.name, patchedBytecode)
+ return patchedClass.getDeclaredConstructor().newInstance() as BeforeHooksTargetContract
+}
+
+private fun getPatchedBeforeHooksTargetInstance(): BeforeHooksTargetContract {
+ val originalBytecode = classToBytecode(BeforeHooksTarget::class.java)
+ val patchedBytecode = applyBeforeHooks(originalBytecode)
+ // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection.
+ val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR")
+ File("$outDir/${BeforeHooksTarget::class.java.simpleName}.class").writeBytes(originalBytecode)
+ File("$outDir/${BeforeHooksTarget::class.java.simpleName}.patched.class").writeBytes(patchedBytecode)
+ val patchedClass = bytecodeToClass(BeforeHooksTarget::class.java.name, patchedBytecode)
+ return patchedClass.getDeclaredConstructor().newInstance() as BeforeHooksTargetContract
+}
+
+class BeforeHookTest {
+
+ @Test
+ fun testBeforeHooksOriginal() {
+ assertSelfCheck(getOriginalBeforeHooksTargetInstance(), false)
+ }
+
+ @Test
+ fun testBeforeHooksNoHooks() {
+ assertSelfCheck(getNoHooksBeforeHooksTargetInstance(), false)
+ }
+
+ @Test
+ fun testBeforeHooksPatched() {
+ assertSelfCheck(getPatchedBeforeHooksTargetInstance(), true)
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTarget.java
new file mode 100644
index 00000000..869e04bf
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTarget.java
@@ -0,0 +1,61 @@
+// 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.instrumentor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+// selfCheck() only passes with the hooks in BeforeHooks.java applied.
+public class BeforeHooksTarget implements BeforeHooksTargetContract {
+ static private int timesCalled = 0;
+ Map<String, Boolean> results = new HashMap<>();
+ Boolean func1Called = false;
+ Boolean funcWithArgsCalled = false;
+
+ static Integer getTimesCalled() {
+ return ++timesCalled;
+ }
+
+ public Map<String, Boolean> selfCheck() {
+ results = new HashMap<>();
+
+ results.put("hasFunc1BeenCalled", hasFunc1BeenCalled());
+
+ timesCalled = 0;
+ results.put("hasBeenCalledTwice", getTimesCalled() == 2);
+
+ if (!results.containsKey("hasBeenCalledWithArgs")) {
+ results.put("hasBeenCalledWithArgs", hasFuncWithArgsBeenCalled(true, "foo"));
+ }
+
+ return results;
+ }
+
+ public void func1() {
+ func1Called = true;
+ }
+
+ private boolean hasFunc1BeenCalled() {
+ return func1Called;
+ }
+
+ public void setFuncWithArgsCalled(Boolean val) {
+ funcWithArgsCalled = val;
+ }
+
+ private boolean hasFuncWithArgsBeenCalled(Boolean boolArgument, String stringArgument) {
+ return funcWithArgsCalled;
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTargetContract.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTargetContract.java
new file mode 100644
index 00000000..61f79dcc
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTargetContract.java
@@ -0,0 +1,25 @@
+// 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.instrumentor;
+
+/**
+ * Helper interface used to call methods on instances of BeforeHooksTarget classes loaded via
+ * different class loaders.
+ */
+public interface BeforeHooksTargetContract extends DynamicTestContract {
+ void func1();
+
+ void setFuncWithArgsCalled(Boolean val);
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationSpecialCasesTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationSpecialCasesTarget.java
new file mode 100644
index 00000000..cb811803
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationSpecialCasesTarget.java
@@ -0,0 +1,41 @@
+// 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.instrumentor;
+
+import java.util.Random;
+
+public class CoverageInstrumentationSpecialCasesTarget {
+ public ReturnClass newAfterJump() {
+ if (new Random().nextBoolean()) {
+ throw new RuntimeException("");
+ }
+ return new ReturnClass(new Random().nextBoolean() ? "foo" : "bar");
+ }
+
+ public int newAndTryCatch() {
+ new Random();
+ try {
+ new Random();
+ return 2;
+ } catch (RuntimeException e) {
+ new Random();
+ return 1;
+ }
+ }
+
+ public static class ReturnClass {
+ public ReturnClass(String content) {}
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTarget.java
new file mode 100644
index 00000000..7502481d
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTarget.java
@@ -0,0 +1,67 @@
+// 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.instrumentor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class CoverageInstrumentationTarget implements DynamicTestContract {
+ volatile int int1 = 3;
+ volatile int int2 = 213234;
+
+ @Override
+ public Map<String, Boolean> selfCheck() {
+ HashMap<String, Boolean> results = new HashMap<>();
+
+ results.put("for0", false);
+ results.put("for1", false);
+ results.put("for2", false);
+ results.put("for3", false);
+ results.put("for4", false);
+ results.put("foobar", false);
+ results.put("baz", true);
+
+ if (int1 < int2) {
+ results.put("block1", true);
+ } else {
+ results.put("block2", false);
+ }
+
+ for (int i = 0; i < 2; i++) {
+ for (int j = 0; j < 5; j++) {
+ results.put("for" + j, i != 0);
+ }
+ }
+
+ foo(results);
+
+ return results;
+ }
+
+ private void foo(HashMap<String, Boolean> results) {
+ bar(results);
+ }
+
+ // The use of Map instead of HashMap is deliberate here: Since Map#put can throw exceptions, the
+ // invocation should be instrumented for coverage.
+ private void bar(Map<String, Boolean> results) {
+ results.put("foobar", true);
+ }
+
+ @SuppressWarnings("unused")
+ private void baz(HashMap<String, Boolean> results) {
+ results.put("baz", false);
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt
new file mode 100644
index 00000000..15c88f4c
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt
@@ -0,0 +1,141 @@
+// 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.instrumentor
+
+import org.junit.Test
+import java.io.File
+import kotlin.test.assertEquals
+
+private fun applyInstrumentation(bytecode: ByteArray): ByteArray {
+ return EdgeCoverageInstrumentor(0, MockCoverageMap::class.java).instrument(bytecode)
+}
+
+private fun getOriginalInstrumentationTargetInstance(): DynamicTestContract {
+ return CoverageInstrumentationTarget()
+}
+
+private fun getInstrumentedInstrumentationTargetInstance(): DynamicTestContract {
+ val originalBytecode = classToBytecode(CoverageInstrumentationTarget::class.java)
+ val patchedBytecode = applyInstrumentation(originalBytecode)
+ // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection.
+ val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR")
+ File("$outDir/${CoverageInstrumentationTarget::class.java.simpleName}.class").writeBytes(originalBytecode)
+ File("$outDir/${CoverageInstrumentationTarget::class.java.simpleName}.patched.class").writeBytes(patchedBytecode)
+ val patchedClass = bytecodeToClass(CoverageInstrumentationTarget::class.java.name, patchedBytecode)
+ return patchedClass.getDeclaredConstructor().newInstance() as DynamicTestContract
+}
+
+private fun assertControlFlow(expectedLocations: List<Int>) {
+ assertEquals(expectedLocations, MockCoverageMap.locations.toList())
+}
+
+class CoverageInstrumentationTest {
+
+ private val constructorReturn = 0
+ private val ifFirstBranch = 1
+ @Suppress("unused")
+ private val ifSecondBranch = 2
+ private val ifEnd = 3
+ private val outerForCondition = 4
+ private val innerForBodyIfFirstRun = 6
+ private val innerForBodyIfSecondRun = 5
+ private val innerForIncrementCounter = 7
+ private val outerForIncrementCounter = 8
+ private val afterFooInvocation = 9
+ private val beforeReturn = 10
+ private val fooAfterBarInvocation = 11
+ private val fooBeforeReturn = 12
+ private val barAfterMapPutInvocation = 13
+ private val barBeforeReturn = 14
+ @Suppress("unused")
+ private val bazReturn = 15
+
+ @Test
+ fun testOriginal() {
+ assertSelfCheck(getOriginalInstrumentationTargetInstance())
+ }
+
+ @Test
+ fun testInstrumented() {
+ MockCoverageMap.clear()
+ assertSelfCheck(getInstrumentedInstrumentationTargetInstance())
+
+ val innerForFirstRunControlFlow = mutableListOf<Int>().apply {
+ repeat(5) {
+ addAll(listOf(innerForBodyIfFirstRun, innerForIncrementCounter))
+ }
+ }.toList()
+ val innerForSecondRunControlFlow = mutableListOf<Int>().apply {
+ repeat(5) {
+ addAll(listOf(innerForBodyIfSecondRun, innerForIncrementCounter))
+ }
+ }.toList()
+ val outerForControlFlow =
+ listOf(outerForCondition) +
+ innerForFirstRunControlFlow +
+ listOf(outerForIncrementCounter, outerForCondition) +
+ innerForSecondRunControlFlow +
+ listOf(outerForIncrementCounter)
+
+ assertControlFlow(
+ listOf(constructorReturn, ifFirstBranch, ifEnd) +
+ outerForControlFlow +
+ listOf(
+ barAfterMapPutInvocation, barBeforeReturn,
+ fooAfterBarInvocation, fooBeforeReturn,
+ afterFooInvocation, beforeReturn
+ )
+ )
+ }
+
+ @OptIn(ExperimentalUnsignedTypes::class)
+ @Test
+ fun testCounters() {
+ MockCoverageMap.clear()
+
+ val target = getInstrumentedInstrumentationTargetInstance()
+ // The constructor of the target is run only once.
+ val takenOnceEdge = constructorReturn
+ // Control flows through the first if branch once per run.
+ val takenOnEveryRunEdge = ifFirstBranch
+
+ var lastCounter = 0.toUByte()
+ for (i in 1..600) {
+ assertSelfCheck(target)
+ assertEquals(1, MockCoverageMap.mem[takenOnceEdge])
+ // Verify that the counter increments, but is never zero.
+ val expectedCounter = (lastCounter + 1U).toUByte().takeUnless { it == 0.toUByte() }
+ ?: (lastCounter + 2U).toUByte()
+ lastCounter = expectedCounter
+ val actualCounter = MockCoverageMap.mem[takenOnEveryRunEdge].toUByte()
+ assertEquals(expectedCounter, actualCounter, "After $i runs:")
+ }
+ }
+
+ @Test
+ fun testSpecialCases() {
+ val originalBytecode = classToBytecode(CoverageInstrumentationSpecialCasesTarget::class.java)
+ val patchedBytecode = applyInstrumentation(originalBytecode)
+ // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection.
+ val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR")
+ File("$outDir/${CoverageInstrumentationSpecialCasesTarget::class.simpleName}.class").writeBytes(originalBytecode)
+ File("$outDir/${CoverageInstrumentationSpecialCasesTarget::class.simpleName}.patched.class").writeBytes(
+ patchedBytecode
+ )
+ val patchedClass = bytecodeToClass(CoverageInstrumentationSpecialCasesTarget::class.java.name, patchedBytecode)
+ // Trigger a class load
+ patchedClass.declaredMethods
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtilsTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtilsTest.kt
new file mode 100644
index 00000000..e7e1feba
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtilsTest.kt
@@ -0,0 +1,73 @@
+// 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.instrumentor
+
+import com.code_intelligence.jazzer.utils.descriptor
+import org.junit.Test
+import kotlin.test.assertEquals
+
+class DescriptorUtilsTest {
+
+ @Test
+ fun testClassDescriptor() {
+ assertEquals("V", java.lang.Void::class.javaPrimitiveType?.descriptor)
+ assertEquals("J", java.lang.Long::class.javaPrimitiveType?.descriptor)
+ assertEquals("[[[Z", Array<Array<BooleanArray>>::class.java.descriptor)
+ assertEquals("[Ljava/lang/String;", Array<String>::class.java.descriptor)
+ }
+
+ @Test
+ fun testExtractInternalClassName() {
+ assertEquals("java/lang/String", extractInternalClassName("Ljava/lang/String;"))
+ assertEquals("[Ljava/lang/String;", extractInternalClassName("[Ljava/lang/String;"))
+ assertEquals("B", extractInternalClassName("B"))
+ }
+
+ @Test
+ fun testExtractTypeDescriptors() {
+ val testCases = listOf(
+ Triple(
+ String::class.java.getMethod("equals", Object::class.java),
+ listOf("Ljava/lang/Object;"),
+ "Z"
+ ),
+ Triple(
+ String::class.java.getMethod("regionMatches", Boolean::class.javaPrimitiveType, Int::class.javaPrimitiveType, String::class.java, Int::class.javaPrimitiveType, Integer::class.javaPrimitiveType),
+ listOf("Z", "I", "Ljava/lang/String;", "I", "I"),
+ "Z"
+ ),
+ Triple(
+ String::class.java.getMethod("getChars", Integer::class.javaPrimitiveType, Int::class.javaPrimitiveType, CharArray::class.java, Int::class.javaPrimitiveType),
+ listOf("I", "I", "[C", "I"),
+ "V"
+ ),
+ Triple(
+ String::class.java.getMethod("subSequence", Integer::class.javaPrimitiveType, Integer::class.javaPrimitiveType),
+ listOf("I", "I"),
+ "Ljava/lang/CharSequence;"
+ ),
+ Triple(
+ String::class.java.getConstructor(),
+ emptyList(),
+ "V"
+ )
+ )
+ for ((executable, parameterDescriptors, returnTypeDescriptor) in testCases) {
+ val descriptor = executable.descriptor
+ assertEquals(extractParameterTypeDescriptors(descriptor), parameterDescriptors)
+ assertEquals(extractReturnTypeDescriptor(descriptor), returnTypeDescriptor)
+ }
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/DynamicTestContract.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/DynamicTestContract.java
new file mode 100644
index 00000000..163b226a
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/DynamicTestContract.java
@@ -0,0 +1,21 @@
+// 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.instrumentor;
+
+import java.util.Map;
+
+public interface DynamicTestContract {
+ Map<String, Boolean> selfCheck();
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt
new file mode 100644
index 00000000..7e7c31c9
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt
@@ -0,0 +1,38 @@
+// 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.instrumentor
+
+import com.code_intelligence.jazzer.api.MethodHook
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class HookValidationTest {
+ @Test
+ fun testValidHooks() {
+ assertEquals(6, loadHooks(ValidHookMocks::class.java).size)
+ }
+
+ @Test
+ fun testInvalidHooks() {
+ for (method in InvalidHookMocks::class.java.methods) {
+ if (method.isAnnotationPresent(MethodHook::class.java)) {
+ assertFailsWith<IllegalArgumentException>("Expected ${method.name} to be an invalid hook") {
+ Hook.verifyAndGetHook(method, method.declaredAnnotations.first() as MethodHook)
+ }
+ }
+ }
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java
new file mode 100644
index 00000000..2723ad6e
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java
@@ -0,0 +1,61 @@
+// 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.instrumentor;
+
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+
+class InvalidHookMocks {
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.String", targetMethod = "equals")
+ public static void incorrectHookIdType(
+ MethodHandle method, String thisObject, Object[] arguments, long hookId) {}
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals")
+ private static void invalidAfterHook(MethodHandle method, String thisObject, Object[] arguments,
+ int hookId, Boolean returnValue) {}
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals")
+ public void invalidAfterHook2(MethodHandle method, String thisObject, Object[] arguments,
+ int hookId, boolean returnValue) {}
+
+ @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.String",
+ targetMethod = "equals", targetMethodDescriptor = "(Ljava/lang/Object;)Z")
+ public static String
+ incorrectReturnType(MethodHandle method, String thisObject, Object[] arguments, int hookId) {
+ return "foo";
+ }
+
+ @MethodHook(
+ type = HookType.REPLACE, targetClassName = "java.lang.String", targetMethod = "equals")
+ public static boolean
+ invalidReplaceHook2(MethodHandle method, Integer thisObject, Object[] arguments, int hookId) {
+ return true;
+ }
+
+ @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.StringBuilder",
+ targetMethod = "<init>", targetMethodDescriptor = "(Ljava/lang/String;)V")
+ public static Object
+ invalidReturnType(MethodHandle method, Object thisObject, Object[] arguments, int hookId)
+ throws Throwable {
+ return null;
+ }
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String",
+ targetMethod = "startsWith", targetMethodDescriptor = "(Ljava/lang/String;)Z")
+ public static void
+ primitiveReturnValueMustBeWrapped(MethodHandle method, String thisObject, Object[] arguments,
+ int hookId, boolean returnValue) {}
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java
new file mode 100644
index 00000000..787ea493
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java
@@ -0,0 +1,45 @@
+// 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.instrumentor;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class MockCoverageMap {
+ public static final int SIZE = 65536;
+ public static final ByteBuffer mem = ByteBuffer.allocate(SIZE);
+ public static int prev_location = 0; // is used in byte code directly
+
+ private static final ByteBuffer previous_mem = ByteBuffer.allocate(SIZE);
+ public static ArrayList<Integer> locations = new ArrayList<>();
+
+ public static void updated() {
+ int updated_pos = -1;
+ for (int i = 0; i < SIZE; i++) {
+ if (previous_mem.get(i) != mem.get(i)) {
+ updated_pos = i;
+ }
+ }
+ locations.add(updated_pos);
+ System.arraycopy(mem.array(), 0, previous_mem.array(), 0, SIZE);
+ }
+
+ public static void clear() {
+ Arrays.fill(mem.array(), (byte) 0);
+ Arrays.fill(previous_mem.array(), (byte) 0);
+ locations.clear();
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockTraceDataFlowCallbacks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockTraceDataFlowCallbacks.java
new file mode 100644
index 00000000..ad659da0
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockTraceDataFlowCallbacks.java
@@ -0,0 +1,106 @@
+// 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.instrumentor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SuppressWarnings("unused")
+public class MockTraceDataFlowCallbacks {
+ private static List<String> hookCalls;
+ private static int assertedCalls;
+
+ public static void init() {
+ hookCalls = new ArrayList<>();
+ assertedCalls = 0;
+ }
+
+ public static boolean hookCall(String expectedCall) {
+ if (assertedCalls >= hookCalls.size()) {
+ System.err.println("Not seen (" + hookCalls.size() + " calls, but " + (assertedCalls + 1)
+ + " expected): " + expectedCall);
+ return false;
+ }
+
+ if (!hookCalls.get(assertedCalls).equals(expectedCall)) {
+ System.err.println("Call " + expectedCall + " not seen, got " + hookCalls.get(assertedCalls));
+ return false;
+ }
+
+ assertedCalls++;
+ return true;
+ }
+
+ public static boolean finish() {
+ if (assertedCalls == hookCalls.size())
+ return true;
+ System.err.println("The following calls were not asserted:");
+ for (int i = assertedCalls; i < hookCalls.size(); i++) {
+ System.err.println(hookCalls.get(i));
+ }
+
+ return false;
+ }
+
+ public static void traceCmpLong(long arg1, long arg2, int pc) {
+ hookCalls.add("LCMP: " + Math.min(arg1, arg2) + ", " + Math.max(arg1, arg2));
+ }
+
+ public static void traceCmpInt(int arg1, int arg2, int pc) {
+ hookCalls.add("ICMP: " + Math.min(arg1, arg2) + ", " + Math.max(arg1, arg2));
+ }
+
+ public static void traceConstCmpInt(int arg1, int arg2, int pc) {
+ hookCalls.add("CICMP: " + arg1 + ", " + arg2);
+ }
+
+ public static void traceDivInt(int val, int pc) {
+ hookCalls.add("IDIV: " + val);
+ }
+
+ public static void traceDivLong(long val, int pc) {
+ hookCalls.add("LDIV: " + val);
+ }
+
+ public static void traceGep(long idx, int pc) {
+ hookCalls.add("GEP: " + idx);
+ }
+
+ public static void traceSwitch(long switchValue, long[] libfuzzerCaseValues, int pc) {
+ if (libfuzzerCaseValues.length < 3
+ // number of case values must match length
+ || libfuzzerCaseValues[0] != libfuzzerCaseValues.length - 2
+ // bit size of case values is always 32 (int)
+ || libfuzzerCaseValues[1] != 32) {
+ hookCalls.add("INVALID_SWITCH");
+ return;
+ }
+
+ StringBuilder builder = new StringBuilder("SWITCH: " + switchValue + ", (");
+ for (int i = 2; i < libfuzzerCaseValues.length; i++) {
+ builder.append(libfuzzerCaseValues[i]);
+ builder.append(", ");
+ }
+ builder.append(")");
+ hookCalls.add(builder.toString());
+ }
+
+ public static int traceCmpLongWrapper(long value1, long value2, int pc) {
+ traceCmpLong(value1, value2, pc);
+ // Long.compare serves as a substitute for the lcmp opcode here
+ // (behaviour is the same)
+ return Long.compare(value1, value2);
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt
new file mode 100644
index 00000000..f286d03f
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt
@@ -0,0 +1,53 @@
+// 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.instrumentor
+
+fun classToBytecode(targetClass: Class<*>): ByteArray {
+ return ClassLoader
+ .getSystemClassLoader()
+ .getResourceAsStream("${targetClass.name.replace('.', '/')}.class")!!
+ .use {
+ it.readBytes()
+ }
+}
+
+fun bytecodeToClass(name: String, bytecode: ByteArray): Class<*> {
+ return BytecodeClassLoader(name, bytecode).loadClass(name)
+}
+
+/**
+ * A ClassLoader that dynamically loads a single specified class from byte code and delegates all other class loads to
+ * its own ClassLoader.
+ */
+class BytecodeClassLoader(val className: String, private val classBytecode: ByteArray) :
+ ClassLoader(BytecodeClassLoader::class.java.classLoader) {
+ override fun loadClass(name: String): Class<*> {
+ if (name != className)
+ return super.loadClass(name)
+
+ return defineClass(className, classBytecode, 0, classBytecode.size)
+ }
+}
+
+fun assertSelfCheck(target: DynamicTestContract, shouldPass: Boolean = true) {
+ val results = target.selfCheck()
+ for ((test, passed) in results) {
+ if (shouldPass) {
+ assert(passed) { "$test should pass" }
+ } else {
+ assert(!passed) { "$test should not pass" }
+ }
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java
new file mode 100644
index 00000000..a71e1180
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java
@@ -0,0 +1,109 @@
+// 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.instrumentor;
+
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+
+public class ReplaceHooks {
+ @MethodHook(type = HookType.REPLACE,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget",
+ targetMethod = "shouldReturnTrue1")
+ public static boolean
+ patchShouldReturnTrue1(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ return true;
+ }
+
+ @MethodHook(type = HookType.REPLACE,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget",
+ targetMethod = "shouldReturnTrue2")
+ public static Boolean
+ patchShouldReturnTrue2(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ return true;
+ }
+
+ @MethodHook(type = HookType.REPLACE,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget",
+ targetMethod = "shouldReturnTrue3")
+ public static Object
+ patchShouldReturnTrue3(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ return true;
+ }
+
+ @MethodHook(type = HookType.REPLACE,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget",
+ targetMethod = "shouldReturnFalse1")
+ public static Boolean
+ patchShouldReturnFalse1(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ return false;
+ }
+
+ @MethodHook(type = HookType.REPLACE,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget",
+ targetMethod = "shouldReturnFalse2")
+ @MethodHook(type = HookType.REPLACE,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget",
+ targetMethod = "shouldReturnFalse3")
+ public static Object
+ patchShouldReturnFalse2(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ return false;
+ }
+
+ @MethodHook(type = HookType.REPLACE,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget",
+ targetMethod = "shouldReturnReversed",
+ targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/String;")
+ public static String
+ patchShouldReturnReversed(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ return new StringBuilder((String) arguments[0]).reverse().toString();
+ }
+
+ @MethodHook(type = HookType.REPLACE,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget",
+ targetMethod = "shouldIncrement")
+ public static int
+ patchShouldIncrement(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ return ((int) arguments[0]) + 1;
+ }
+
+ @MethodHook(type = HookType.REPLACE,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget",
+ targetMethod = "shouldCallPass")
+ public static void
+ patchShouldCallPass(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ ((ReplaceHooksTargetContract) thisObject).pass("shouldCallPass");
+ }
+
+ @MethodHook(type = HookType.REPLACE,
+ targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget",
+ targetMethod = "idempotent", targetMethodDescriptor = "(I)I")
+ public static int
+ patchIdempotent(MethodHandle method, Object thisObject, Object[] arguments, int hookId)
+ throws Throwable {
+ // Iterate the function twice to pass the test.
+ int input = (int) arguments[0];
+ int temp = (int) method.invokeWithArguments(thisObject, input);
+ return (int) method.invokeWithArguments(thisObject, temp);
+ }
+
+ @MethodHook(type = HookType.REPLACE, targetClassName = "java.util.AbstractList",
+ targetMethod = "get", targetMethodDescriptor = "(I)Ljava/lang/Object;")
+ public static Object
+ patchAbstractListGet(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ return true;
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt
new file mode 100644
index 00000000..76fb53e5
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt
@@ -0,0 +1,63 @@
+// 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.instrumentor
+
+import org.junit.Test
+import java.io.File
+
+private fun applyReplaceHooks(bytecode: ByteArray): ByteArray {
+ return HookInstrumentor(loadHooks(ReplaceHooks::class.java), false).instrument(bytecode)
+}
+
+private fun getOriginalReplaceHooksTargetInstance(): ReplaceHooksTargetContract {
+ return ReplaceHooksTarget()
+}
+
+private fun getNoHooksReplaceHooksTargetInstance(): ReplaceHooksTargetContract {
+ val originalBytecode = classToBytecode(ReplaceHooksTarget::class.java)
+ // Let the bytecode pass through the hooking logic, but don't apply any hooks.
+ val patchedBytecode = HookInstrumentor(emptyList(), false).instrument(originalBytecode)
+ val patchedClass = bytecodeToClass(ReplaceHooksTarget::class.java.name, patchedBytecode)
+ return patchedClass.getDeclaredConstructor().newInstance() as ReplaceHooksTargetContract
+}
+
+private fun getPatchedReplaceHooksTargetInstance(): ReplaceHooksTargetContract {
+ val originalBytecode = classToBytecode(ReplaceHooksTarget::class.java)
+ val patchedBytecode = applyReplaceHooks(originalBytecode)
+ // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection.
+ val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR")
+ File("$outDir/${ReplaceHooksTarget::class.java.simpleName}.class").writeBytes(originalBytecode)
+ File("$outDir/${ReplaceHooksTarget::class.java.simpleName}.patched.class").writeBytes(patchedBytecode)
+ val patchedClass = bytecodeToClass(ReplaceHooksTarget::class.java.name, patchedBytecode)
+ return patchedClass.getDeclaredConstructor().newInstance() as ReplaceHooksTargetContract
+}
+
+class ReplaceHookTest {
+
+ @Test
+ fun testReplaceHooksOriginal() {
+ assertSelfCheck(getOriginalReplaceHooksTargetInstance(), false)
+ }
+
+ @Test
+ fun testReplaceHooksNoHooks() {
+ assertSelfCheck(getNoHooksReplaceHooksTargetInstance(), false)
+ }
+
+ @Test
+ fun testReplaceHooksPatched() {
+ assertSelfCheck(getPatchedReplaceHooksTargetInstance(), true)
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java
new file mode 100644
index 00000000..7a4b89f8
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java
@@ -0,0 +1,120 @@
+// 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.instrumentor;
+
+import java.security.SecureRandom;
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+// selfCheck() only passes with the hooks in ReplaceHooks.java applied.
+public class ReplaceHooksTarget implements ReplaceHooksTargetContract {
+ Map<String, Boolean> results = new HashMap<>();
+
+ public static boolean shouldReturnTrue3() {
+ // return true;
+ return false;
+ }
+
+ public Map<String, Boolean> selfCheck() {
+ results = new HashMap<>();
+
+ results.put("shouldReturnTrue1", shouldReturnTrue1());
+ results.put("shouldReturnTrue2", shouldReturnTrue2());
+ results.put("shouldReturnTrue3", shouldReturnTrue3());
+ try {
+ boolean notTrue = false;
+ results.put("shouldReturnFalse1", notTrue);
+ if (!results.get("shouldReturnFalse1"))
+ results.put("shouldReturnFalse1", !shouldReturnFalse1());
+ boolean notFalse = true;
+ results.put("shouldReturnFalse2", !shouldReturnFalse2() && notFalse);
+ results.put("shouldReturnFalse3", !shouldReturnFalse3());
+ } catch (Exception e) {
+ boolean notTrue = false;
+ results.put("shouldNotBeExecuted", notTrue);
+ }
+ results.put("shouldReturnReversed", shouldReturnReversed("foo").equals("oof"));
+ results.put("shouldIncrement", shouldIncrement(5) == 6);
+ results.put("verifyIdentity", verifyIdentity());
+
+ results.put("shouldCallPass", false);
+ if (!results.get("shouldCallPass")) {
+ shouldCallPass();
+ }
+
+ AbstractList<Boolean> boolList = new ArrayList<>();
+ boolList.add(false);
+ results.put("arrayListGet", boolList.get(0));
+
+ return results;
+ }
+
+ public boolean shouldReturnTrue1() {
+ // return true;
+ return false;
+ }
+
+ public boolean shouldReturnTrue2() {
+ // return true;
+ return false;
+ }
+
+ protected Boolean shouldReturnFalse1() {
+ // return false;
+ return true;
+ }
+
+ Boolean shouldReturnFalse2() {
+ // return false;
+ return true;
+ }
+
+ public Boolean shouldReturnFalse3() {
+ // return false;
+ return true;
+ }
+
+ public String shouldReturnReversed(String input) {
+ // return new StringBuilder(input).reverse().toString();
+ return input;
+ }
+
+ public int shouldIncrement(int input) {
+ // return input + 1;
+ return input;
+ }
+
+ private void shouldCallPass() {
+ // pass("shouldCallPass");
+ }
+
+ private boolean verifyIdentity() {
+ SecureRandom rand = new SecureRandom();
+ int input = rand.nextInt();
+ // return idempotent(idempotent(input)) == input;
+ return idempotent(input) == input;
+ }
+
+ private int idempotent(int input) {
+ int secret = 0x12345678;
+ return input ^ secret;
+ }
+
+ public void pass(String test) {
+ results.put(test, true);
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTargetContract.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTargetContract.java
new file mode 100644
index 00000000..e3dff93e
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTargetContract.java
@@ -0,0 +1,23 @@
+// 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.instrumentor;
+
+/**
+ * Helper interface used to call methods on instances of ReplaceHooksTarget classes loaded via
+ * different class loaders.
+ */
+public interface ReplaceHooksTargetContract extends DynamicTestContract {
+ void pass(String test);
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java
new file mode 100644
index 00000000..48f16e60
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java
@@ -0,0 +1,152 @@
+// 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.instrumentor;
+
+import java.nio.ByteBuffer;
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+import java.util.Vector;
+
+public class TraceDataFlowInstrumentationTarget implements DynamicTestContract {
+ volatile long long1 = 1;
+ volatile long long2 = 1;
+ volatile long long3 = 2;
+ volatile long long4 = 3;
+
+ volatile int int1 = 4;
+ volatile int int2 = 4;
+ volatile int int3 = 6;
+ volatile int int4 = 5;
+
+ volatile int switchValue = 1200;
+
+ @Override
+ public Map<String, Boolean> selfCheck() {
+ Map<String, Boolean> results = new HashMap<>();
+
+ results.put("longCompareEq", long1 == long2);
+ results.put("longCompareNe", long3 != long4);
+
+ results.put("intCompareEq", int1 == int2);
+ results.put("intCompareNe", int3 != int4);
+ results.put("intCompareLt", int4 < int3);
+ results.put("intCompareLe", int4 <= int3);
+ results.put("intCompareGt", int3 > int4);
+ results.put("intCompareGe", int3 >= int4);
+
+ // Not instrumented since all case values are non-negative and < 256.
+ switch (switchValue) {
+ case 119:
+ case 120:
+ case 121:
+ results.put("tableSwitchUninstrumented", false);
+ break;
+ default:
+ results.put("tableSwitchUninstrumented", true);
+ }
+
+ // Not instrumented since all case values are non-negative and < 256.
+ switch (switchValue) {
+ case 1:
+ case 200:
+ results.put("lookupSwitchUninstrumented", false);
+ break;
+ default:
+ results.put("lookupSwitchUninstrumented", true);
+ }
+
+ results.put("emptySwitchUninstrumented", false);
+ switch (switchValue) {
+ default:
+ results.put("emptySwitchUninstrumented", true);
+ }
+
+ switch (switchValue) {
+ case 1000:
+ case 1001:
+ // case 1002: The tableswitch instruction will contain a gap case for 1002.
+ case 1003:
+ results.put("tableSwitch", false);
+ break;
+ default:
+ results.put("tableSwitch", true);
+ }
+
+ switch (-switchValue) {
+ case -1200:
+ results.put("lookupSwitch", true);
+ break;
+ case -1:
+ case -10:
+ case -1000:
+ case 200:
+ default:
+ results.put("lookupSwitch", false);
+ }
+
+ results.put("intDiv", (int3 / 2) == 3);
+
+ results.put("longDiv", (long4 / 2) == 1);
+
+ String[] referenceArray = {"foo", "foo", "bar"};
+ boolean[] boolArray = {false, false, true};
+ byte[] byteArray = {0, 0, 2};
+ char[] charArray = {0, 0, 0, 3};
+ double[] doubleArray = {0, 0, 0, 0, 4};
+ float[] floatArray = {0, 0, 0, 0, 0, 5};
+ int[] intArray = {0, 0, 0, 0, 0, 0, 6};
+ long[] longArray = {0, 0, 0, 0, 0, 0, 0, 7};
+ short[] shortArray = {0, 0, 0, 0, 0, 0, 0, 0, 8};
+
+ results.put("referenceArrayGep", referenceArray[2].equals("bar"));
+ results.put("boolArrayGep", boolArray[2]);
+ results.put("byteArrayGep", byteArray[2] == 2);
+ results.put("charArrayGep", charArray[3] == 3);
+ results.put("doubleArrayGep", doubleArray[4] == 4);
+ results.put("floatArrayGep", floatArray[5] == 5);
+ results.put("intArrayGep", intArray[6] == 6);
+ results.put("longArrayGep", longArray[7] == 7);
+ results.put("shortArrayGep", shortArray[8] == 8);
+
+ ByteBuffer buffer = ByteBuffer.allocate(100);
+ buffer.get(2);
+ buffer.getChar(3);
+ buffer.getDouble(4);
+ buffer.getFloat(5);
+ buffer.getInt(6);
+ buffer.getLong(7);
+ buffer.getShort(8);
+
+ "foobarbazbat".charAt(9);
+ "foobarbazbat".codePointAt(10);
+ new StringBuilder("foobarbazbat").charAt(11);
+
+ (new Vector<>(Collections.nCopies(20, "foo"))).get(12);
+ (new ArrayList<>(Collections.nCopies(20, "foo"))).get(13);
+ Stack<String> stack = new Stack<>();
+ for (int i = 0; i < 20; i++) stack.push("foo");
+ stack.get(14);
+ stack.get(15);
+ ((AbstractList<String>) stack).get(16);
+ ((List<String>) stack).get(17);
+
+ return results;
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt
new file mode 100644
index 00000000..c6fd218f
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt
@@ -0,0 +1,145 @@
+// 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.instrumentor
+
+import org.junit.Test
+import java.io.File
+
+private fun applyInstrumentation(bytecode: ByteArray): ByteArray {
+ return TraceDataFlowInstrumentor(
+ setOf(
+ InstrumentationType.CMP,
+ InstrumentationType.DIV,
+ InstrumentationType.GEP
+ ),
+ MockTraceDataFlowCallbacks::class.java
+ ).instrument(bytecode)
+}
+
+private fun getOriginalInstrumentationTargetInstance(): DynamicTestContract {
+ return TraceDataFlowInstrumentationTarget()
+}
+
+private fun getInstrumentedInstrumentationTargetInstance(): DynamicTestContract {
+ val originalBytecode = classToBytecode(TraceDataFlowInstrumentationTarget::class.java)
+ val patchedBytecode = applyInstrumentation(originalBytecode)
+ // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection.
+ val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR")
+ File("$outDir/${TraceDataFlowInstrumentationTarget::class.simpleName}.class").writeBytes(originalBytecode)
+ File("$outDir/${TraceDataFlowInstrumentationTarget::class.simpleName}.patched.class").writeBytes(patchedBytecode)
+ val patchedClass = bytecodeToClass(TraceDataFlowInstrumentationTarget::class.java.name, patchedBytecode)
+ return patchedClass.getDeclaredConstructor().newInstance() as DynamicTestContract
+}
+
+class TraceDataFlowInstrumentationTest {
+
+ @Test
+ fun testOriginal() {
+ MockTraceDataFlowCallbacks.init()
+ assertSelfCheck(getOriginalInstrumentationTargetInstance())
+ assert(MockTraceDataFlowCallbacks.finish())
+ }
+
+ @Test
+ fun testInstrumented() {
+ MockTraceDataFlowCallbacks.init()
+ assertSelfCheck(getInstrumentedInstrumentationTargetInstance())
+ listOf(
+ // long compares
+ "LCMP: 1, 1",
+ "LCMP: 2, 3",
+ // int compares
+ "ICMP: 4, 4",
+ "ICMP: 5, 6",
+ "ICMP: 5, 6",
+ "ICMP: 5, 6",
+ "ICMP: 5, 6",
+ "ICMP: 5, 6",
+ // tableswitch with gap
+ "SWITCH: 1200, (1000, 1001, 1003, )",
+ // lookupswitch
+ "SWITCH: -1200, (200, -1200, -1000, -10, -1, )",
+ // (6 / 2) == 3
+ "IDIV: 2",
+ "ICMP: 3, 3",
+ // (3 / 2) == 1
+ "LDIV: 2",
+ "LCMP: 1, 1",
+ // referenceArray[2]
+ "GEP: 2",
+ // boolArray[2]
+ "GEP: 2",
+ // byteArray[2] == 2
+ "GEP: 2",
+ "ICMP: 2, 2",
+ // charArray[3] == 3
+ "GEP: 3",
+ "ICMP: 3, 3",
+ // doubleArray[4] == 4
+ "GEP: 4",
+ // floatArray[5] == 5
+ "GEP: 5",
+ "CICMP: 0, 0",
+ // intArray[6] == 6
+ "GEP: 6",
+ "ICMP: 6, 6",
+ // longArray[7] == 7
+ "GEP: 7",
+ "LCMP: 7, 7",
+ // shortArray[8] == 8
+ "GEP: 8",
+ "ICMP: 8, 8",
+
+ "GEP: 2",
+ "GEP: 3",
+ "GEP: 4",
+ "GEP: 5",
+ "GEP: 6",
+ "GEP: 7",
+ "GEP: 8",
+ "GEP: 9",
+ "GEP: 10",
+ "GEP: 11",
+ "GEP: 12",
+ "GEP: 13",
+ "ICMP: 0, 20",
+ "ICMP: 1, 20",
+ "ICMP: 2, 20",
+ "ICMP: 3, 20",
+ "ICMP: 4, 20",
+ "ICMP: 5, 20",
+ "ICMP: 6, 20",
+ "ICMP: 7, 20",
+ "ICMP: 8, 20",
+ "ICMP: 9, 20",
+ "ICMP: 10, 20",
+ "ICMP: 11, 20",
+ "ICMP: 12, 20",
+ "ICMP: 13, 20",
+ "ICMP: 14, 20",
+ "ICMP: 15, 20",
+ "ICMP: 16, 20",
+ "ICMP: 17, 20",
+ "ICMP: 18, 20",
+ "ICMP: 19, 20",
+ "ICMP: 20, 20",
+ "GEP: 14",
+ "GEP: 15",
+ "GEP: 16",
+ "GEP: 17",
+ ).forEach { assert(MockTraceDataFlowCallbacks.hookCall(it)) }
+ assert(MockTraceDataFlowCallbacks.finish())
+ }
+}
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java
new file mode 100644
index 00000000..06bed141
--- /dev/null
+++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java
@@ -0,0 +1,49 @@
+// 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.instrumentor;
+
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+
+class ValidHookMocks {
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.String", targetMethod = "equals")
+ public static void validBeforeHook(
+ MethodHandle method, String thisObject, Object[] arguments, int hookId) {}
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals")
+ public static void validAfterHook(MethodHandle method, String thisObject, Object[] arguments,
+ int hookId, Boolean returnValue) {}
+
+ @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals")
+ public static void validAfterHook2(MethodHandle method, String thisObject, Object[] arguments,
+ int hookId, boolean returnValue) {}
+
+ @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.String",
+ targetMethod = "equals", targetMethodDescriptor = "(Ljava/lang/Object;)Z")
+ public static Boolean
+ validReplaceHook(MethodHandle method, String thisObject, Object[] arguments, int hookId) {
+ return true;
+ }
+
+ @MethodHook(
+ type = HookType.REPLACE, targetClassName = "java.lang.String", targetMethod = "equals")
+ @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.String",
+ targetMethod = "equalsIgnoreCase")
+ public static boolean
+ validReplaceHook2(MethodHandle method, String thisObject, Object[] arguments, int hookId) {
+ return true;
+ }
+}