aboutsummaryrefslogtreecommitdiff
path: root/driver/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'driver/src/main/java')
-rw-r--r--driver/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel64
-rw-r--r--driver/src/main/java/com/code_intelligence/jazzer/driver/Driver.java111
-rw-r--r--driver/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java450
-rw-r--r--driver/src/main/java/com/code_intelligence/jazzer/driver/Opt.java173
-rw-r--r--driver/src/main/java/com/code_intelligence/jazzer/driver/Reproducer.java.tmpl28
-rw-r--r--driver/src/main/java/com/code_intelligence/jazzer/driver/ReproducerTemplate.java85
-rw-r--r--driver/src/main/java/com/code_intelligence/jazzer/driver/Utils.java39
7 files changed, 950 insertions, 0 deletions
diff --git a/driver/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel b/driver/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel
new file mode 100644
index 00000000..c8e6ba1e
--- /dev/null
+++ b/driver/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel
@@ -0,0 +1,64 @@
+load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library")
+
+java_library(
+ name = "driver",
+ srcs = [":Driver.java"],
+ visibility = [
+ "//agent:__pkg__",
+ ],
+ deps = [
+ ":fuzz_target_runner",
+ ":opt",
+ ":utils",
+ "//agent/src/main/java/com/code_intelligence/jazzer/agent:agent_lib",
+ "@net_bytebuddy_byte_buddy_agent//jar",
+ ],
+)
+
+java_jni_library(
+ name = "fuzz_target_runner",
+ srcs = ["FuzzTargetRunner.java"],
+ native_libs = [
+ "//driver/src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver",
+ ],
+ visibility = [
+ "//agent:__pkg__",
+ "//driver/src/main/native/com/code_intelligence/jazzer/driver:__pkg__",
+ "//driver/src/test:__subpackages__",
+ ],
+ deps = [
+ ":opt",
+ ":reproducer_template",
+ ":utils",
+ "//agent/src/main/java/com/code_intelligence/jazzer/api",
+ "//agent/src/main/java/com/code_intelligence/jazzer/autofuzz",
+ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor",
+ "//agent/src/main/java/com/code_intelligence/jazzer/runtime",
+ "//agent/src/main/java/com/code_intelligence/jazzer/runtime:coverage_map",
+ "//agent/src/main/java/com/code_intelligence/jazzer/runtime:fuzzed_data_provider",
+ "//agent/src/main/java/com/code_intelligence/jazzer/runtime:signal_handler",
+ "//agent/src/main/java/com/code_intelligence/jazzer/runtime:unsafe_provider",
+ "//agent/src/main/java/com/code_intelligence/jazzer/utils",
+ ],
+)
+
+java_library(
+ name = "reproducer_template",
+ srcs = ["ReproducerTemplate.java"],
+ resources = ["Reproducer.java.tmpl"],
+ deps = [":opt"],
+)
+
+java_library(
+ name = "opt",
+ srcs = ["Opt.java"],
+ visibility = [
+ "//agent/src/main/java/com/code_intelligence/jazzer:__subpackages__",
+ "//driver/src/test/java/com/code_intelligence/jazzer/driver:__pkg__",
+ ],
+)
+
+java_library(
+ name = "utils",
+ srcs = ["Utils.java"],
+)
diff --git a/driver/src/main/java/com/code_intelligence/jazzer/driver/Driver.java b/driver/src/main/java/com/code_intelligence/jazzer/driver/Driver.java
new file mode 100644
index 00000000..5b107ad8
--- /dev/null
+++ b/driver/src/main/java/com/code_intelligence/jazzer/driver/Driver.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2022 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.driver;
+
+import static java.lang.System.err;
+
+import com.code_intelligence.jazzer.agent.Agent;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.SecureRandom;
+import java.util.List;
+import net.bytebuddy.agent.ByteBuddyAgent;
+
+public class Driver {
+ // Accessed from jazzer_main.cpp.
+ @SuppressWarnings("unused")
+ private static int start(byte[][] nativeArgs) throws IOException {
+ List<String> args = Utils.fromNativeArgs(nativeArgs);
+
+ final boolean spawnsSubprocesses = args.stream().anyMatch(
+ arg -> arg.startsWith("-fork=") || arg.startsWith("-jobs=") || arg.startsWith("-merge="));
+ if (spawnsSubprocesses) {
+ if (!System.getProperty("jazzer.coverage_report", "").isEmpty()) {
+ err.println(
+ "WARN: --coverage_report does not support parallel fuzzing and has been disabled");
+ System.clearProperty("jazzer.coverage_report");
+ }
+ if (!System.getProperty("jazzer.coverage_dump", "").isEmpty()) {
+ err.println(
+ "WARN: --coverage_dump does not support parallel fuzzing and has been disabled");
+ System.clearProperty("jazzer.coverage_dump");
+ }
+
+ String idSyncFileArg = System.getProperty("jazzer.id_sync_file", "");
+ Path idSyncFile;
+ if (idSyncFileArg.isEmpty()) {
+ // Create an empty temporary file used for coverage ID synchronization and
+ // pass its path to the agent in every child process. This requires adding
+ // the argument to argv for it to be picked up by libFuzzer, which then
+ // forwards it to child processes.
+ idSyncFile = Files.createTempFile("jazzer-", "");
+ args.add("--id_sync_file=" + idSyncFile.toAbsolutePath());
+ } else {
+ // Creates the file, truncating it if it exists.
+ idSyncFile = Files.write(Paths.get(idSyncFileArg), new byte[] {});
+ }
+ // This wouldn't run in case we exit the process with _Exit, but the parent process of a -fork
+ // run is expected to exit with a regular exit(0), which does cause JVM shutdown hooks to run:
+ // https://github.com/llvm/llvm-project/blob/940e178c0018b32af2f1478d331fc41a92a7dac7/compiler-rt/lib/fuzzer/FuzzerFork.cpp#L491
+ idSyncFile.toFile().deleteOnExit();
+ }
+
+ // Jazzer's hooks use deterministic randomness and thus require a seed. Search for the last
+ // occurrence of a "-seed" argument as that is the one that is used by libFuzzer. If none is
+ // set, generate one and pass it to libFuzzer so that a fuzzing run can be reproduced simply by
+ // setting the seed printed by libFuzzer.
+ String seed = args.stream().reduce(
+ null, (prev, cur) -> cur.startsWith("-seed=") ? cur.substring("-seed=".length()) : prev);
+ if (seed == null) {
+ seed = Integer.toUnsignedString(new SecureRandom().nextInt());
+ // Only add the -seed argument to the command line if not running in a mode
+ // that spawns subprocesses. These would inherit the same seed, which might
+ // make them less effective.
+ if (!spawnsSubprocesses) {
+ args.add("-seed=" + seed);
+ }
+ }
+ System.setProperty("jazzer.seed", seed);
+
+ if (args.stream().noneMatch(arg -> arg.startsWith("-rss_limit_mb="))) {
+ args.add(getDefaultRssLimitMbArg());
+ }
+
+ // Do *not* modify system properties beyond this point - initializing Opt parses them as a side
+ // effect.
+
+ if (Opt.hooks) {
+ Agent.premain(null, ByteBuddyAgent.install());
+ }
+
+ return FuzzTargetRunner.startLibFuzzer(args);
+ }
+
+ private static String getDefaultRssLimitMbArg() {
+ // Java OutOfMemoryErrors are strictly more informative than libFuzzer's out of memory crashes.
+ // We thus want to scale the default libFuzzer memory limit, which includes all memory used by
+ // the process including Jazzer's native and non-native memory footprint, such that:
+ // 1. we never reach it purely by allocating memory on the Java heap;
+ // 2. it is still reached if the fuzz target allocates excessively on the native heap.
+ // As a heuristic, we set the overall memory limit to 2 * the maximum size of the Java heap and
+ // add a fixed 1 GiB on top for the fuzzer's own memory usage.
+ long maxHeapInBytes = Runtime.getRuntime().maxMemory();
+ return "-rss_limit_mb=" + ((2 * maxHeapInBytes / (1024 * 1024)) + 1024);
+ }
+}
diff --git a/driver/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java b/driver/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java
new file mode 100644
index 00000000..5646e91a
--- /dev/null
+++ b/driver/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright 2022 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.driver;
+
+import static java.lang.System.err;
+import static java.lang.System.exit;
+import static java.lang.System.out;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.autofuzz.FuzzTarget;
+import com.code_intelligence.jazzer.instrumentor.CoverageRecorder;
+import com.code_intelligence.jazzer.runtime.CoverageMap;
+import com.code_intelligence.jazzer.runtime.FuzzedDataProviderImpl;
+import com.code_intelligence.jazzer.runtime.JazzerInternal;
+import com.code_intelligence.jazzer.runtime.RecordingFuzzedDataProvider;
+import com.code_intelligence.jazzer.runtime.SignalHandler;
+import com.code_intelligence.jazzer.runtime.UnsafeProvider;
+import com.code_intelligence.jazzer.utils.ExceptionUtils;
+import com.code_intelligence.jazzer.utils.ManifestUtils;
+import com.github.fmeum.rules_jni.RulesJni;
+import java.io.IOException;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import sun.misc.Unsafe;
+
+/**
+ * Executes a fuzz target and reports findings.
+ *
+ * <p>This class maintains global state (both native and non-native) and thus cannot be used
+ * concurrently.
+ */
+public final class FuzzTargetRunner {
+ static {
+ RulesJni.loadLibrary("jazzer_driver", FuzzTargetRunner.class);
+ }
+
+ private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe();
+ private static final long BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class);
+
+ // Default value of the libFuzzer -error_exitcode flag.
+ private static final int LIBFUZZER_ERROR_EXIT_CODE = 77;
+ private static final String AUTOFUZZ_FUZZ_TARGET =
+ "com.code_intelligence.jazzer.autofuzz.FuzzTarget";
+ private static final String FUZZER_TEST_ONE_INPUT = "fuzzerTestOneInput";
+ private static final String FUZZER_INITIALIZE = "fuzzerInitialize";
+ private static final String FUZZER_TEARDOWN = "fuzzerTearDown";
+
+ private static final Set<Long> ignoredTokens = new HashSet<>(Opt.ignore);
+ private static final FuzzedDataProviderImpl fuzzedDataProvider =
+ FuzzedDataProviderImpl.withNativeData();
+ private static final Class<?> fuzzTargetClass;
+ private static final MethodHandle fuzzTarget;
+ public static final boolean useFuzzedDataProvider;
+ private static final ReproducerTemplate reproducerTemplate;
+
+ static {
+ String targetClassName = determineFuzzTargetClassName();
+
+ // FuzzTargetRunner is loaded by the bootstrap class loader since Driver installs the agent
+ // before invoking FuzzTargetRunner.startLibFuzzer. We can't load the fuzz target with that
+ // class loader - we have to use the class loader that loaded Driver. This would be
+ // straightforward to do in Java 9+, but requires the use of reflection to maintain
+ // compatibility with Java 8, which doesn't have StackWalker.
+ //
+ // Note that we can't just move the agent initialization so that FuzzTargetRunner is loaded by
+ // Driver's class loader: The agent and FuzzTargetRunner have to share the native library that
+ // contains libFuzzer and that library needs to be available in the bootstrap class loader
+ // since instrumentation applied to Java standard library classes still needs to be able to call
+ // libFuzzer hooks. A fundamental JNI restriction is that a native library can't be shared
+ // between two different class loaders, so FuzzTargetRunner is thus forced to be loaded in the
+ // bootstrap class loader, which makes this ugly code block necessary.
+ // We also can't use the system class loader since Driver may be loaded by a custom class loader
+ // if not invoked from the native driver.
+ Class<?> driverClass;
+ try {
+ Class<?> reflectionClass = Class.forName("sun.reflect.Reflection");
+ try {
+ driverClass =
+ (Class<?>) reflectionClass.getMethod("getCallerClass", int.class).invoke(null, 2);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ throw new IllegalStateException(e);
+ }
+ } catch (ClassNotFoundException e) {
+ // sun.reflect.Reflection is no longer available after Java 8, use StackWalker.
+ try {
+ Class<?> stackWalker = Class.forName("java.lang.StackWalker");
+ Class<? extends Enum<?>> stackWalkerOption =
+ (Class<? extends Enum<?>>) Class.forName("java.lang.StackWalker$Option");
+ Enum<?> retainClassReferences =
+ Arrays.stream(stackWalkerOption.getEnumConstants())
+ .filter(v -> v.name().equals("RETAIN_CLASS_REFERENCE"))
+ .findFirst()
+ .orElseThrow(()
+ -> new IllegalStateException(
+ "No RETAIN_CLASS_REFERENCE in java.lang.StackWalker$Option"));
+ Object stackWalkerInstance = stackWalker.getMethod("getInstance", stackWalkerOption)
+ .invoke(null, retainClassReferences);
+ Method stackWalkerGetCallerClass = stackWalker.getMethod("getCallerClass");
+ driverClass = (Class<?>) stackWalkerGetCallerClass.invoke(stackWalkerInstance);
+ } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException
+ | InvocationTargetException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ try {
+ ClassLoader driverClassLoader = driverClass.getClassLoader();
+ driverClassLoader.setDefaultAssertionStatus(true);
+ fuzzTargetClass = Class.forName(targetClassName, false, driverClassLoader);
+ } catch (ClassNotFoundException e) {
+ err.print("ERROR: ");
+ e.printStackTrace(err);
+ exit(1);
+ throw new IllegalStateException("Not reached");
+ }
+ // Inform the agent about the fuzz target class. Important note: This has to be done *before*
+ // the class is initialized so that hooks can enable themselves in time for the fuzz target's
+ // static initializer.
+ JazzerInternal.onFuzzTargetReady(targetClassName);
+
+ Method bytesFuzzTarget = targetPublicStaticMethodOrNull(FUZZER_TEST_ONE_INPUT, byte[].class);
+ Method dataFuzzTarget =
+ targetPublicStaticMethodOrNull(FUZZER_TEST_ONE_INPUT, FuzzedDataProvider.class);
+ if ((bytesFuzzTarget != null) == (dataFuzzTarget != null)) {
+ err.printf(
+ "ERROR: %s must define exactly one of the following two functions:%n", targetClassName);
+ err.println("public static void fuzzerTestOneInput(byte[] ...)");
+ err.println("public static void fuzzerTestOneInput(FuzzedDataProvider ...)");
+ err.println(
+ "Note: Fuzz targets returning boolean are no longer supported; exceptions should be thrown instead of returning true.");
+ exit(1);
+ }
+ try {
+ if (bytesFuzzTarget != null) {
+ useFuzzedDataProvider = false;
+ fuzzTarget = MethodHandles.publicLookup().unreflect(bytesFuzzTarget);
+ } else {
+ useFuzzedDataProvider = true;
+ fuzzTarget = MethodHandles.publicLookup().unreflect(dataFuzzTarget);
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ reproducerTemplate = new ReproducerTemplate(fuzzTargetClass.getName(), useFuzzedDataProvider);
+
+ Method initializeNoArgs = targetPublicStaticMethodOrNull(FUZZER_INITIALIZE);
+ Method initializeWithArgs = targetPublicStaticMethodOrNull(FUZZER_INITIALIZE, String[].class);
+ try {
+ if (initializeWithArgs != null) {
+ initializeWithArgs.invoke(null, (Object) Opt.targetArgs.toArray(new String[] {}));
+ } else if (initializeNoArgs != null) {
+ initializeNoArgs.invoke(null);
+ }
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ err.print("== Java Exception in fuzzerInitialize: ");
+ e.printStackTrace(err);
+ exit(1);
+ }
+
+ if (Opt.hooks) {
+ // libFuzzer will clear the coverage map after this method returns and keeps no record of the
+ // coverage accumulated so far (e.g. by static initializers). We record it here to keep it
+ // around for JaCoCo coverage reports.
+ CoverageRecorder.updateCoveredIdsWithCoverageMap();
+ }
+
+ Runtime.getRuntime().addShutdownHook(new Thread(FuzzTargetRunner::shutdown));
+ }
+
+ /**
+ * A test-only convenience wrapper around {@link #runOne(long, int)}.
+ */
+ static int runOne(byte[] data) {
+ long dataPtr = UNSAFE.allocateMemory(data.length);
+ UNSAFE.copyMemory(data, BYTE_ARRAY_OFFSET, null, dataPtr, data.length);
+ try {
+ return runOne(dataPtr, data.length);
+ } finally {
+ UNSAFE.freeMemory(dataPtr);
+ }
+ }
+
+ /**
+ * Executes the user-provided fuzz target once.
+ *
+ * @param dataPtr a native pointer to beginning of the input provided by the fuzzer for this
+ * execution
+ * @param dataLength length of the fuzzer input
+ * @return the value that the native LLVMFuzzerTestOneInput function should return. Currently,
+ * this is always 0. The function may exit the process instead of returning.
+ */
+ private static int runOne(long dataPtr, int dataLength) {
+ Throwable finding = null;
+ byte[] data = null;
+ try {
+ if (useFuzzedDataProvider) {
+ fuzzedDataProvider.setNativeData(dataPtr, dataLength);
+ fuzzTarget.invokeExact((FuzzedDataProvider) fuzzedDataProvider);
+ } else {
+ data = copyToArray(dataPtr, dataLength);
+ fuzzTarget.invokeExact(data);
+ }
+ } catch (Throwable uncaughtFinding) {
+ finding = uncaughtFinding;
+ }
+ // Explicitly reported findings take precedence over uncaught exceptions.
+ if (JazzerInternal.lastFinding != null) {
+ finding = JazzerInternal.lastFinding;
+ JazzerInternal.lastFinding = null;
+ }
+ if (finding == null) {
+ return 0;
+ }
+ if (Opt.hooks) {
+ finding = ExceptionUtils.preprocessThrowable(finding);
+ }
+
+ long dedupToken = Opt.dedup ? ExceptionUtils.computeDedupToken(finding) : 0;
+ // Opt.keepGoing implies Opt.dedup.
+ if (Opt.keepGoing > 1 && !ignoredTokens.add(dedupToken)) {
+ return 0;
+ }
+
+ err.println();
+ err.print("== Java Exception: ");
+ finding.printStackTrace(err);
+ if (Opt.dedup) {
+ // Has to be printed to stdout as it is parsed by libFuzzer when minimizing a crash. It does
+ // not necessarily have to appear at the beginning of a line.
+ // https://github.com/llvm/llvm-project/blob/4c106c93eb68f8f9f201202677cd31e326c16823/compiler-rt/lib/fuzzer/FuzzerDriver.cpp#L342
+ out.printf(Locale.ROOT, "DEDUP_TOKEN: %016x%n", dedupToken);
+ }
+ err.println("== libFuzzer crashing input ==");
+ printCrashingInput();
+ // dumpReproducer needs to be called after libFuzzer printed its final stats as otherwise it
+ // would report incorrect coverage - the reproducer generation involved rerunning the fuzz
+ // target.
+ dumpReproducer(data);
+
+ if (Opt.keepGoing == 1 || Long.compareUnsigned(ignoredTokens.size(), Opt.keepGoing) >= 0) {
+ // Reached the maximum amount of findings to keep going for, crash after shutdown. We use
+ // _Exit rather than System.exit to not trigger libFuzzer's exit handlers.
+ shutdown();
+ _Exit(LIBFUZZER_ERROR_EXIT_CODE);
+ throw new IllegalStateException("Not reached");
+ }
+ return 0;
+ }
+
+ /*
+ * Starts libFuzzer via LLVMFuzzerRunDriver.
+ *
+ * Note: Must be public rather than package-private as it is loaded in a different class loader
+ * than Driver.
+ */
+ public static int startLibFuzzer(List<String> args) {
+ SignalHandler.initialize();
+ return startLibFuzzer(Utils.toNativeArgs(args));
+ }
+
+ private static void shutdown() {
+ if (!Opt.coverageDump.isEmpty() || !Opt.coverageReport.isEmpty()) {
+ int[] everCoveredIds = CoverageMap.getEverCoveredIds();
+ if (!Opt.coverageDump.isEmpty()) {
+ CoverageRecorder.dumpJacocoCoverage(everCoveredIds, Opt.coverageDump);
+ }
+ if (!Opt.coverageReport.isEmpty()) {
+ CoverageRecorder.dumpCoverageReport(everCoveredIds, Opt.coverageReport);
+ }
+ }
+
+ Method teardown = targetPublicStaticMethodOrNull(FUZZER_TEARDOWN);
+ if (teardown == null) {
+ return;
+ }
+ err.println("calling fuzzerTearDown function");
+ try {
+ teardown.invoke(null);
+ } catch (InvocationTargetException e) {
+ // An exception in fuzzerTearDown is a regular finding.
+ err.print("== Java Exception in fuzzerTearDown: ");
+ e.getCause().printStackTrace(err);
+ _Exit(LIBFUZZER_ERROR_EXIT_CODE);
+ } catch (Throwable t) {
+ // Any other exception is an error.
+ t.printStackTrace(err);
+ _Exit(1);
+ }
+ }
+
+ private static String determineFuzzTargetClassName() {
+ if (!Opt.autofuzz.isEmpty()) {
+ return AUTOFUZZ_FUZZ_TARGET;
+ }
+ if (!Opt.targetClass.isEmpty()) {
+ return Opt.targetClass;
+ }
+ String manifestTargetClass = ManifestUtils.detectFuzzTargetClass();
+ if (manifestTargetClass != null) {
+ return manifestTargetClass;
+ }
+ err.println("Missing argument --target_class=<fuzz_target_class>");
+ exit(1);
+ throw new IllegalStateException("Not reached");
+ }
+
+ private static void dumpReproducer(byte[] data) {
+ if (data == null) {
+ assert useFuzzedDataProvider;
+ fuzzedDataProvider.reset();
+ data = fuzzedDataProvider.consumeRemainingAsBytes();
+ }
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("SHA-1 not available", e);
+ }
+ String dataSha1 = toHexString(digest.digest(data));
+
+ if (!Opt.autofuzz.isEmpty()) {
+ fuzzedDataProvider.reset();
+ FuzzTarget.dumpReproducer(fuzzedDataProvider, Opt.reproducerPath, dataSha1);
+ return;
+ }
+
+ String base64Data;
+ if (useFuzzedDataProvider) {
+ fuzzedDataProvider.reset();
+ FuzzedDataProvider recordingFuzzedDataProvider =
+ RecordingFuzzedDataProvider.makeFuzzedDataProviderProxy(fuzzedDataProvider);
+ try {
+ fuzzTarget.invokeExact(recordingFuzzedDataProvider);
+ if (JazzerInternal.lastFinding == null) {
+ err.println("Failed to reproduce crash when rerunning with recorder");
+ }
+ } catch (Throwable ignored) {
+ // Expected.
+ }
+ try {
+ base64Data = RecordingFuzzedDataProvider.serializeFuzzedDataProviderProxy(
+ recordingFuzzedDataProvider);
+ } catch (IOException e) {
+ err.print("ERROR: Failed to create reproducer: ");
+ e.printStackTrace(err);
+ // Don't let libFuzzer print a native stack trace.
+ _Exit(1);
+ throw new IllegalStateException("Not reached");
+ }
+ } else {
+ base64Data = Base64.getEncoder().encodeToString(data);
+ }
+
+ reproducerTemplate.dumpReproducer(base64Data, dataSha1);
+ }
+
+ private static Method targetPublicStaticMethodOrNull(String name, Class<?>... parameterTypes) {
+ try {
+ Method method = fuzzTargetClass.getMethod(name, parameterTypes);
+ if (!Modifier.isStatic(method.getModifiers()) || !Modifier.isPublic(method.getModifiers())) {
+ return null;
+ }
+ return method;
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Convert a byte array to a lower-case hex string.
+ *
+ * <p>The returned hex string always has {@code 2 * bytes.length} characters.
+ *
+ * @param bytes the bytes to convert
+ * @return a lower-case hex string representing the bytes
+ */
+ private static String toHexString(byte[] bytes) {
+ String unpadded = new BigInteger(1, bytes).toString(16);
+ int numLeadingZeroes = 2 * bytes.length - unpadded.length();
+ return String.join("", Collections.nCopies(numLeadingZeroes, "0")) + unpadded;
+ }
+
+ // Accessed by fuzz_target_runner.cpp.
+ @SuppressWarnings("unused")
+ private static void dumpAllStackTraces() {
+ ExceptionUtils.dumpAllStackTraces();
+ }
+
+ private static byte[] copyToArray(long ptr, int length) {
+ // TODO: Use Unsafe.allocateUninitializedArray instead once Java 9 is the base.
+ byte[] array = new byte[length];
+ UNSAFE.copyMemory(null, ptr, array, BYTE_ARRAY_OFFSET, length);
+ return array;
+ }
+
+ /**
+ * Starts libFuzzer via LLVMFuzzerRunDriver.
+ *
+ * @param args command-line arguments encoded in UTF-8 (not null-terminated)
+ * @return the return value of LLVMFuzzerRunDriver
+ */
+ private static native int startLibFuzzer(byte[][] args);
+
+ /**
+ * Causes libFuzzer to write the current input to disk as a crashing input and emit some
+ * information about it to stderr.
+ */
+ private static native void printCrashingInput();
+
+ /**
+ * Immediately terminates the process without performing any cleanup.
+ *
+ * <p>Neither JVM shutdown hooks nor native exit handlers are called. This method does not return.
+ *
+ * <p>This method provides a way to exit Jazzer without triggering libFuzzer's exit hook that
+ * prints the "fuzz target exited" error message. It should thus be preferred over
+ * {@link System#exit} in any situation where Jazzer encounters an error after the fuzz target has
+ * started running.
+ *
+ * @param exitCode the exit code
+ */
+ private static native void _Exit(int exitCode);
+}
diff --git a/driver/src/main/java/com/code_intelligence/jazzer/driver/Opt.java b/driver/src/main/java/com/code_intelligence/jazzer/driver/Opt.java
new file mode 100644
index 00000000..477c7d38
--- /dev/null
+++ b/driver/src/main/java/com/code_intelligence/jazzer/driver/Opt.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2022 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.driver;
+
+import static java.lang.System.err;
+import static java.lang.System.exit;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Static options that determine the runtime behavior of the fuzzer, set via Java properties.
+ *
+ * <p>Each option corresponds to a command-line argument of the driver of the same name.
+ *
+ * <p>Every public field should be deeply immutable.
+ *
+ * <p>This class is loaded twice: As it is used in {@link FuzzTargetRunner}, it is loaded in the
+ * class loader that loads {@link Driver}. It is also used in
+ * {@link com.code_intelligence.jazzer.agent.Agent} after the agent JAR has been added to the
+ * bootstrap classpath and thus is loaded again in the bootstrap loader. This is not a problem since
+ * it only provides immutable fields and has no non-fatal side effects.
+ */
+public final class Opt {
+ private static final char SYSTEM_DELIMITER =
+ System.getProperty("os.name").startsWith("Windows") ? ';' : ':';
+
+ public static final String autofuzz = stringSetting("autofuzz", "");
+ public static final List<String> autofuzzIgnore = stringListSetting("autofuzz_ignore", ',');
+ public static final String coverageDump = stringSetting("coverage_dump", "");
+ public static final String coverageReport = stringSetting("coverage_report", "");
+ public static final List<String> customHookIncludes = stringListSetting("custom_hook_includes");
+ public static final List<String> customHookExcludes = stringListSetting("custom_hook_excludes");
+ public static final List<String> customHooks = stringListSetting("custom_hooks");
+ public static final List<String> disabledHooks = stringListSetting("disabled_hooks");
+ public static final String dumpClassesDir = stringSetting("dump_classes_dir", "");
+ public static final boolean hooks = boolSetting("hooks", true);
+ public static final String idSyncFile = stringSetting("id_sync_file", null);
+ public static final List<String> instrumentationIncludes =
+ stringListSetting("instrumentation_includes");
+ public static final List<String> instrumentationExcludes =
+ stringListSetting("instrumentation_excludes");
+ public static final Set<Long> ignore =
+ Collections.unmodifiableSet(stringListSetting("ignore", ',')
+ .stream()
+ .map(Long::parseUnsignedLong)
+ .collect(Collectors.toSet()));
+ public static final String reproducerPath = stringSetting("reproducer_path", ".");
+ public static final String targetClass = stringSetting("target_class", "");
+ public static final List<String> trace = stringListSetting("trace");
+
+ // The values of these settings depend on autofuzz.
+ public static final List<String> targetArgs = autofuzz.isEmpty()
+ ? stringListSetting("target_args", ' ')
+ : Collections.unmodifiableList(
+ Stream.concat(Stream.of(autofuzz), autofuzzIgnore.stream()).collect(Collectors.toList()));
+ public static final long keepGoing =
+ uint64Setting("keep_going", autofuzz.isEmpty() ? 1 : Long.MAX_VALUE);
+
+ // Default to false if hooks is false to mimic the original behavior of the native fuzz target
+ // runner, but still support hooks = false && dedup = true.
+ public static final boolean dedup = boolSetting("dedup", hooks);
+
+ static {
+ if (!targetClass.isEmpty() && !autofuzz.isEmpty()) {
+ err.println("--target_class and --autofuzz cannot be specified together");
+ exit(1);
+ }
+ if (!stringListSetting("target_args", ' ').isEmpty() && !autofuzz.isEmpty()) {
+ err.println("--target_args and --autofuzz cannot be specified together");
+ exit(1);
+ }
+ if (autofuzz.isEmpty() && !autofuzzIgnore.isEmpty()) {
+ err.println("--autofuzz_ignore requires --autofuzz");
+ exit(1);
+ }
+ if ((!ignore.isEmpty() || keepGoing > 1) && !dedup) {
+ // --autofuzz implicitly sets keepGoing to Integer.MAX_VALUE.
+ err.println("--nodedup is not supported with --ignore, --keep_going, or --autofuzz");
+ exit(1);
+ }
+ }
+
+ private static final String optionsPrefix = "jazzer.";
+
+ private static String stringSetting(String name, String defaultValue) {
+ return System.getProperty(optionsPrefix + name, defaultValue);
+ }
+
+ private static List<String> stringListSetting(String name) {
+ return stringListSetting(name, SYSTEM_DELIMITER);
+ }
+
+ private static List<String> stringListSetting(String name, char separator) {
+ String value = System.getProperty(optionsPrefix + name);
+ if (value == null || value.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return splitOnUnescapedSeparator(value, separator);
+ }
+
+ private static boolean boolSetting(String name, boolean defaultValue) {
+ String value = System.getProperty(optionsPrefix + name);
+ if (value == null) {
+ return defaultValue;
+ }
+ return Boolean.parseBoolean(value);
+ }
+
+ private static long uint64Setting(String name, long defaultValue) {
+ String value = System.getProperty(optionsPrefix + name);
+ if (value == null) {
+ return defaultValue;
+ }
+ return Long.parseUnsignedLong(value, 10);
+ }
+
+ /**
+ * Split value into non-empty takens separated by separator. Backslashes can be used to escape
+ * separators (or backslashes).
+ *
+ * @param value the string to split
+ * @param separator a single character to split on (backslash is not allowed)
+ * @return an immutable list of tokens obtained by splitting value on separator
+ */
+ static List<String> splitOnUnescapedSeparator(String value, char separator) {
+ if (separator == '\\') {
+ throw new IllegalArgumentException("separator '\\' is not supported");
+ }
+ ArrayList<String> tokens = new ArrayList<>();
+ StringBuilder currentToken = new StringBuilder();
+ boolean inEscapeState = false;
+ for (int pos = 0; pos < value.length(); pos++) {
+ char c = value.charAt(pos);
+ if (inEscapeState) {
+ currentToken.append(c);
+ inEscapeState = false;
+ } else if (c == '\\') {
+ inEscapeState = true;
+ } else if (c == separator) {
+ // Do not emit empty tokens between consecutive separators.
+ if (currentToken.length() > 0) {
+ tokens.add(currentToken.toString());
+ }
+ currentToken.setLength(0);
+ } else {
+ currentToken.append(c);
+ }
+ }
+ if (currentToken.length() > 0) {
+ tokens.add(currentToken.toString());
+ }
+ return Collections.unmodifiableList(tokens);
+ }
+}
diff --git a/driver/src/main/java/com/code_intelligence/jazzer/driver/Reproducer.java.tmpl b/driver/src/main/java/com/code_intelligence/jazzer/driver/Reproducer.java.tmpl
new file mode 100644
index 00000000..d9cb1e9e
--- /dev/null
+++ b/driver/src/main/java/com/code_intelligence/jazzer/driver/Reproducer.java.tmpl
@@ -0,0 +1,28 @@
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class Crash_%1$s {
+ static final String base64Bytes = String.join("", "%2$s");
+
+ public static void main(String[] args) throws Throwable {
+ ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true);
+ try {
+ Method fuzzerInitialize = %3$s.class.getMethod("fuzzerInitialize");
+ fuzzerInitialize.invoke(null);
+ } catch (NoSuchMethodException ignored) {
+ try {
+ Method fuzzerInitialize = %3$s.class.getMethod("fuzzerInitialize", String[].class);
+ fuzzerInitialize.invoke(null, (Object) args);
+ } catch (NoSuchMethodException ignored1) {
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ %4$s
+ %3$s.fuzzerTestOneInput(input);
+ }
+}
diff --git a/driver/src/main/java/com/code_intelligence/jazzer/driver/ReproducerTemplate.java b/driver/src/main/java/com/code_intelligence/jazzer/driver/ReproducerTemplate.java
new file mode 100644
index 00000000..0c7721cf
--- /dev/null
+++ b/driver/src/main/java/com/code_intelligence/jazzer/driver/ReproducerTemplate.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2022 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.driver;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.stream.Collectors;
+
+final class ReproducerTemplate {
+ // A constant pool CONSTANT_Utf8_info entry should be able to hold data of size
+ // uint16, but somehow this does not seem to be the case and leads to invalid
+ // code crash reproducer code. Reducing the size by one resolves the problem.
+ private static final int DATA_CHUNK_MAX_LENGTH = Short.MAX_VALUE - 1;
+ private static final String RAW_BYTES_INPUT =
+ "byte[] input = java.util.Base64.getDecoder().decode(base64Bytes);";
+ private static final String FUZZED_DATA_PROVIDER_INPUT =
+ "com.code_intelligence.jazzer.api.CannedFuzzedDataProvider input = new com.code_intelligence.jazzer.api.CannedFuzzedDataProvider(base64Bytes);";
+
+ private final String targetClass;
+ private final boolean useFuzzedDataProvider;
+
+ public ReproducerTemplate(String targetClass, boolean useFuzzedDataProvider) {
+ this.targetClass = targetClass;
+ this.useFuzzedDataProvider = useFuzzedDataProvider;
+ }
+
+ /**
+ * Emits a Java reproducer to {@code Crash_HASH.java} in {@code Opt.reproducerPath}.
+ *
+ * @param data the Base64-encoded data to emit as a string literal
+ * @param sha the SHA1 hash of the raw fuzzer input
+ */
+ public void dumpReproducer(String data, String sha) {
+ String targetArg = useFuzzedDataProvider ? FUZZED_DATA_PROVIDER_INPUT : RAW_BYTES_INPUT;
+ String template = new BufferedReader(
+ new InputStreamReader(ReproducerTemplate.class.getResourceAsStream("Reproducer.java.tmpl"),
+ StandardCharsets.UTF_8))
+ .lines()
+ .collect(Collectors.joining("\n"));
+ String chunkedData = chunkStringLiteral(data);
+ String javaSource = String.format(template, sha, chunkedData, targetClass, targetArg);
+ Path javaPath = Paths.get(Opt.reproducerPath, String.format("Crash_%s.java", sha));
+ try {
+ Files.write(javaPath, javaSource.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE);
+ } catch (IOException e) {
+ System.err.printf("ERROR: Failed to write Java reproducer to %s%n", javaPath);
+ e.printStackTrace();
+ }
+ System.out.printf(
+ "reproducer_path='%s'; Java reproducer written to %s%n", Opt.reproducerPath, javaPath);
+ }
+
+ // The serialization of recorded FuzzedDataProvider invocations can get too long to be emitted
+ // into the template as a single String literal. This is mitigated by chunking the data and
+ // concatenating it again in the generated code.
+ private String chunkStringLiteral(String data) {
+ ArrayList<String> chunks = new ArrayList<>();
+ for (int i = 0; i <= data.length() / DATA_CHUNK_MAX_LENGTH; i++) {
+ chunks.add(data.substring(
+ i * DATA_CHUNK_MAX_LENGTH, Math.min((i + 1) * DATA_CHUNK_MAX_LENGTH, data.length())));
+ }
+ return String.join("\", \"", chunks);
+ }
+}
diff --git a/driver/src/main/java/com/code_intelligence/jazzer/driver/Utils.java b/driver/src/main/java/com/code_intelligence/jazzer/driver/Utils.java
new file mode 100644
index 00000000..37eb1d0f
--- /dev/null
+++ b/driver/src/main/java/com/code_intelligence/jazzer/driver/Utils.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022 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.driver;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class Utils {
+ /**
+ * Convert the arguments to UTF8 before passing them on to JNI as there are no JNI functions to
+ * get (unmodified) UTF-8 out of a jstring.
+ */
+ static byte[][] toNativeArgs(Collection<String> args) {
+ return args.stream().map(str -> str.getBytes(StandardCharsets.UTF_8)).toArray(byte[][] ::new);
+ }
+
+ static List<String> fromNativeArgs(byte[][] args) {
+ return Arrays.stream(args)
+ .map(bytes -> new String(bytes, StandardCharsets.UTF_8))
+ .collect(Collectors.toList());
+ }
+}