diff options
author | Muhammad Haseeb Ahmad <mhahmad@google.com> | 2021-12-30 18:15:58 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2021-12-30 18:15:58 +0000 |
commit | 0f73d9c5add52fa24500a9ddb691528db216e096 (patch) | |
tree | c5880647e8b29782d15be0c99a60e56fed6f8a02 /agent/src | |
parent | b997679abe998d84ad4b9c3e6589342794d3bfcb (diff) | |
parent | 844d7aba71788e3f59411187316eed26ba25c7bd (diff) | |
download | jazzer-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')
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; + } +} |