aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java')
-rw-r--r--src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java542
1 files changed, 542 insertions, 0 deletions
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java
new file mode 100644
index 00000000..aefa5352
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java
@@ -0,0 +1,542 @@
+/*
+ * Copyright 2023 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 com.code_intelligence.jazzer.driver.Constants.JAZZER_FINDING_EXIT_CODE;
+import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID;
+import static java.lang.System.exit;
+import static java.util.stream.Collectors.joining;
+
+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.mutation.ArgumentsMutator;
+import com.code_intelligence.jazzer.runtime.FuzzTargetRunnerNatives;
+import com.code_intelligence.jazzer.runtime.JazzerInternal;
+import com.code_intelligence.jazzer.utils.Log;
+import com.code_intelligence.jazzer.utils.UnsafeProvider;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+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.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+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 java.util.function.Predicate;
+import java.util.stream.Stream;
+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 {
+ private static final String OPENTEST4J_TEST_ABORTED_EXCEPTION =
+ "org.opentest4j.TestAbortedException";
+
+ private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe();
+
+ private static final long BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class);
+
+ // Possible return values for the libFuzzer callback runOne.
+ private static final int LIBFUZZER_CONTINUE = 0;
+ private static final int LIBFUZZER_RETURN_FROM_DRIVER = -2;
+
+ private static boolean invalidCorpusFileWarningShown = false;
+ private static final Set<Long> ignoredTokens = new HashSet<>(Opt.ignore);
+ private static final FuzzedDataProviderImpl fuzzedDataProvider =
+ FuzzedDataProviderImpl.withNativeData();
+ private static final MethodHandle fuzzTargetMethod;
+ private static final boolean useFuzzedDataProvider;
+ // Reused in every iteration analogous to JUnit's PER_CLASS lifecycle.
+ private static final Object fuzzTargetInstance;
+ private static final Method fuzzerTearDown;
+ private static final ArgumentsMutator mutator;
+ private static final ReproducerTemplate reproducerTemplate;
+ private static Predicate<Throwable> findingHandler;
+
+ static {
+ FuzzTargetHolder.FuzzTarget fuzzTarget = FuzzTargetHolder.fuzzTarget;
+ Class<?> fuzzTargetClass = fuzzTarget.method.getDeclaringClass();
+
+ // The method may not be accessible - JUnit test classes and methods are usually declared
+ // without access modifiers and thus package-private.
+ fuzzTarget.method.setAccessible(true);
+ try {
+ fuzzTargetMethod = MethodHandles.lookup().unreflect(fuzzTarget.method);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException(e);
+ }
+ useFuzzedDataProvider = fuzzTarget.usesFuzzedDataProvider();
+ if (!useFuzzedDataProvider && IS_ANDROID) {
+ Log.error("Android fuzz targets must use " + FuzzedDataProvider.class.getName());
+ exit(1);
+ throw new IllegalStateException("Not reached");
+ }
+
+ fuzzerTearDown = fuzzTarget.tearDown.orElse(null);
+ reproducerTemplate = new ReproducerTemplate(fuzzTargetClass.getName(), useFuzzedDataProvider);
+
+ JazzerInternal.onFuzzTargetReady(fuzzTargetClass.getName());
+
+ try {
+ fuzzTargetInstance = fuzzTarget.newInstance.call();
+ } catch (Throwable t) {
+ Log.finding(t);
+ exit(1);
+ throw new IllegalStateException("Not reached");
+ }
+
+ if (Opt.experimentalMutator) {
+ if (Modifier.isStatic(fuzzTarget.method.getModifiers())) {
+ mutator = ArgumentsMutator.forStaticMethodOrThrow(fuzzTarget.method);
+ } else {
+ mutator = ArgumentsMutator.forInstanceMethodOrThrow(fuzzTargetInstance, fuzzTarget.method);
+ }
+ Log.info("Using experimental mutator: " + mutator);
+ } else {
+ mutator = null;
+ }
+
+ 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;
+ Object argument;
+ if (Opt.experimentalMutator) {
+ // TODO: Instead of copying the native data and then reading it in, consider the following
+ // optimizations if they turn out to be worthwhile in benchmarks:
+ // 1. Let libFuzzer pass in a null pointer if the byte array hasn't changed since the last
+ // call to our custom mutator and skip the read entirely.
+ // 2. Implement a InputStream backed by Unsafe to avoid the copyToArray overhead.
+ byte[] buf = copyToArray(dataPtr, dataLength);
+ boolean readExactly = mutator.read(new ByteArrayInputStream(buf));
+
+ // All inputs constructed by the mutator framework can be read exactly, existing corpus files
+ // may not be valid for the current fuzz target anymore, though. In this case, print a warning
+ // once.
+ if (!(invalidCorpusFileWarningShown || readExactly || isFixedLibFuzzerInput(buf))) {
+ invalidCorpusFileWarningShown = true;
+ Log.warn("Some files in the seed corpus do not match the fuzz target signature. "
+ + "This indicates that they were generated with a different signature and may cause issues reproducing previous findings.");
+ }
+ data = null;
+ argument = null;
+ } else if (useFuzzedDataProvider) {
+ fuzzedDataProvider.setNativeData(dataPtr, dataLength);
+ data = null;
+ argument = fuzzedDataProvider;
+ } else {
+ data = copyToArray(dataPtr, dataLength);
+ argument = data;
+ }
+ try {
+ if (Opt.experimentalMutator) {
+ // No need to detach as we are currently reading in the mutator state from bytes in every
+ // iteration.
+ mutator.invoke(false);
+ } else if (fuzzTargetInstance == null) {
+ fuzzTargetMethod.invoke(argument);
+ } else {
+ fuzzTargetMethod.invoke(fuzzTargetInstance, argument);
+ }
+ } catch (Throwable uncaughtFinding) {
+ finding = uncaughtFinding;
+ }
+
+ // When using libFuzzer's -merge flag, only the coverage of the current input is relevant, not
+ // whether it is crashing. Since every crash would cause a restart of the process and thus the
+ // JVM, we can optimize this case by not crashing.
+ //
+ // Incidentally, this makes the behavior of fuzz targets relying on global states more
+ // consistent: Rather than resetting the global state after every crashing input and thus
+ // dependent on the particular ordering of the inputs, we never reset it.
+ if (Opt.mergeInner) {
+ return LIBFUZZER_CONTINUE;
+ }
+
+ // Explicitly reported findings take precedence over uncaught exceptions.
+ if (JazzerInternal.lastFinding != null) {
+ finding = JazzerInternal.lastFinding;
+ JazzerInternal.lastFinding = null;
+ }
+ // Allow skipping invalid inputs in fuzz tests by using e.g. JUnit's assumeTrue.
+ if (finding == null || finding.getClass().getName().equals(OPENTEST4J_TEST_ABORTED_EXCEPTION)) {
+ return LIBFUZZER_CONTINUE;
+ }
+ if (Opt.hooks) {
+ finding = ExceptionUtils.preprocessThrowable(finding);
+ }
+
+ long dedupToken = Opt.dedup ? ExceptionUtils.computeDedupToken(finding) : 0;
+ if (Opt.dedup && !ignoredTokens.add(dedupToken)) {
+ return LIBFUZZER_CONTINUE;
+ }
+
+ if (findingHandler != null) {
+ // We still print the libFuzzer crashing input information, which also dumps the crashing
+ // input as a side effect.
+ printCrashingInput();
+ if (findingHandler.test(finding)) {
+ return LIBFUZZER_CONTINUE;
+ } else {
+ return LIBFUZZER_RETURN_FROM_DRIVER;
+ }
+ }
+
+ // The user-provided fuzz target method has returned. Any further exits are on us and should not
+ // result in a "fuzz target exited" warning being printed by libFuzzer.
+ temporarilyDisableLibfuzzerExitHook();
+
+ Log.finding(finding);
+ 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
+ Log.structuredOutput(String.format(Locale.ROOT, "DEDUP_TOKEN: %016x", dedupToken));
+ }
+ Log.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.
+ // It doesn't support @FuzzTest fuzz targets, but these come with an integrated regression test
+ // that satisfies the same purpose.
+ // It also doesn't support the experimental mutator yet as that requires implementing Java code
+ // generation for mutators.
+ if (fuzzTargetInstance == null && !Opt.experimentalMutator) {
+ dumpReproducer(data);
+ }
+
+ if (!Opt.dedup || 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.
+ if (!Opt.autofuzz.isEmpty() && Opt.dedup) {
+ Log.println("");
+ Log.info(String.format(
+ "To continue fuzzing past this particular finding, rerun with the following additional argument:"
+ + "%n%n --ignore=%s%n%n"
+ + "To ignore all findings of this kind, rerun with the following additional argument:"
+ + "%n%n --autofuzz_ignore=%s",
+ ignoredTokens.stream()
+ .map(token -> Long.toUnsignedString(token, 16))
+ .collect(joining(",")),
+ Stream.concat(Opt.autofuzzIgnore.stream(), Stream.of(finding.getClass().getName()))
+ .collect(joining(","))));
+ }
+ System.exit(JAZZER_FINDING_EXIT_CODE);
+ throw new IllegalStateException("Not reached");
+ }
+ return LIBFUZZER_CONTINUE;
+ }
+
+ private static boolean isFixedLibFuzzerInput(byte[] input) {
+ // Detect special libFuzzer inputs which can not be processed by the mutator framework.
+ // libFuzzer always uses an empty input, and one with a single line feed (10) to indicate
+ // end of initial corpus file processing.
+ return input.length == 0 || (input.length == 1 && input[0] == 10);
+ }
+
+ // Called via JNI, being passed data from LLVMFuzzerCustomMutator.
+ @SuppressWarnings("unused")
+ private static int mutateOne(long data, int size, int maxSize, int seed) {
+ mutate(data, size, seed);
+ return writeToMemory(mutator, data, maxSize);
+ }
+
+ private static void mutate(long data, int size, int seed) {
+ // libFuzzer sends the input "\n" when there are no corpus entries. We use that as a signal to
+ // initialize the mutator instead of just reading that trivial input to produce a more
+ // interesting value.
+ if (size == 1 && UNSAFE.getByte(data) == '\n') {
+ mutator.init(seed);
+ } else {
+ // TODO: See the comment on earlier calls to read for potential optimizations.
+ mutator.read(new ByteArrayInputStream(copyToArray(data, size)));
+ mutator.mutate(seed);
+ }
+ }
+
+ private static long crossOverCount = 0;
+
+ // Called via JNI, being passed data from LLVMFuzzerCustomCrossOver.
+ @SuppressWarnings("unused")
+ private static int crossOver(
+ long data1, int size1, long data2, int size2, long out, int maxOutSize, int seed) {
+ // Custom cross over and custom mutate are the only mutators registered in
+ // libFuzzer, hence cross over is picked as often as mutate, which is way
+ // too frequently. Without custom mutate, cross over would be picked from
+ // the list of default mutators, so ~1/12 of the time. This also seems too
+ // much and is reduced to a configurable frequency, default 1/100, here,
+ // mutate is used in the other cases.
+ if (Opt.experimentalCrossOverFrequency != 0
+ && crossOverCount++ % Opt.experimentalCrossOverFrequency == 0) {
+ mutator.crossOver(new ByteArrayInputStream(copyToArray(data1, size1)),
+ new ByteArrayInputStream(copyToArray(data2, size2)), seed);
+ } else {
+ mutate(data1, size1, seed);
+ }
+ return writeToMemory(mutator, out, maxOutSize);
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static int writeToMemory(ArgumentsMutator mutator, long out, int maxOutSize) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ // TODO: Instead of writing to a byte array and then copying that array's contents into
+ // memory, consider introducing an OutputStream backed by Unsafe.
+ mutator.write(baos);
+ byte[] mutatedBytes = baos.toByteArray();
+ int newSize = Math.min(mutatedBytes.length, maxOutSize);
+ UNSAFE.copyMemory(mutatedBytes, BYTE_ARRAY_OFFSET, null, out, newSize);
+ return newSize;
+ }
+
+ /*
+ * Starts libFuzzer via LLVMFuzzerRunDriver.
+ */
+ public static int startLibFuzzer(List<String> args) {
+ // We always define LLVMFuzzerCustomMutator, but only use it when --experimental_mutator is
+ // specified. libFuzzer contains logic that disables --len_control when it finds the custom
+ // mutator symbol:
+ // https://github.com/llvm/llvm-project/blob/da3623de2411dd931913eb510e94fe846c929c24/compiler-rt/lib/fuzzer/FuzzerDriver.cpp#L202-L207
+ // We thus have to explicitly set --len_control to its default value when not using the new
+ // mutator.
+ // TODO: libFuzzer still emits a message about --len_control being disabled by default even if
+ // we override it via a flag. We may want to patch this out.
+ if (!Opt.experimentalMutator) {
+ // args may not be mutable.
+ args = new ArrayList<>(args);
+ // https://github.com/llvm/llvm-project/blob/da3623de2411dd931913eb510e94fe846c929c24/compiler-rt/lib/fuzzer/FuzzerFlags.def#L19
+ args.add("-len_control=100");
+ }
+
+ for (String arg : args.subList(1, args.size())) {
+ if (!arg.startsWith("-")) {
+ Log.info("using inputs from: " + arg);
+ }
+ }
+
+ if (!IS_ANDROID) {
+ SignalHandler.initialize();
+ }
+ return startLibFuzzer(
+ args.stream().map(str -> str.getBytes(StandardCharsets.UTF_8)).toArray(byte[][] ::new));
+ }
+
+ /**
+ * Registers a custom handler for findings.
+ *
+ * @param findingHandler a consumer for the finding that returns true if the fuzzer should
+ * continue fuzzing and false if it should return from
+ * {@link FuzzTargetRunner#startLibFuzzer(List)}.
+ */
+ public static void registerFindingHandler(Predicate<Throwable> findingHandler) {
+ FuzzTargetRunner.findingHandler = findingHandler;
+ }
+
+ private static void shutdown() {
+ if (!Opt.coverageDump.isEmpty() || !Opt.coverageReport.isEmpty()) {
+ if (!Opt.coverageDump.isEmpty()) {
+ CoverageRecorder.dumpJacocoCoverage(Opt.coverageDump);
+ }
+ if (!Opt.coverageReport.isEmpty()) {
+ CoverageRecorder.dumpCoverageReport(Opt.coverageReport);
+ }
+ }
+
+ if (fuzzerTearDown == null) {
+ return;
+ }
+ Log.info("calling fuzzerTearDown function");
+ try {
+ fuzzerTearDown.invoke(null);
+ } catch (InvocationTargetException e) {
+ Log.finding(e.getCause());
+ System.exit(JAZZER_FINDING_EXIT_CODE);
+ } catch (Throwable t) {
+ Log.error(t);
+ System.exit(1);
+ }
+ }
+
+ 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 {
+ fuzzTargetMethod.invokeExact(recordingFuzzedDataProvider);
+ if (JazzerInternal.lastFinding == null) {
+ Log.warn("Failed to reproduce crash when rerunning with recorder");
+ }
+ } catch (Throwable ignored) {
+ // Expected.
+ }
+ try {
+ base64Data = RecordingFuzzedDataProvider.serializeFuzzedDataProviderProxy(
+ recordingFuzzedDataProvider);
+ } catch (IOException e) {
+ Log.error("Failed to create reproducer", e);
+ // Don't let libFuzzer print a native stack trace.
+ System.exit(1);
+ throw new IllegalStateException("Not reached");
+ }
+ } else {
+ base64Data = Base64.getEncoder().encodeToString(data);
+ }
+
+ reproducerTemplate.dumpReproducer(base64Data, dataSha1);
+ }
+
+ /**
+ * 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 int startLibFuzzer(byte[][] args) {
+ return FuzzTargetRunnerNatives.startLibFuzzer(
+ args, FuzzTargetRunner.class, Opt.experimentalMutator);
+ }
+
+ /**
+ * Causes libFuzzer to write the current input to disk as a crashing input and emit some
+ * information about it to stderr.
+ */
+ public static void printCrashingInput() {
+ FuzzTargetRunnerNatives.printCrashingInput();
+ }
+
+ /**
+ * Returns the debug string of the current mutator.
+ * If no mutator is used, returns null.
+ */
+ public static String mutatorDebugString() {
+ return mutator != null ? mutator.toString() : null;
+ }
+
+ /**
+ * Returns whether the current mutator has detected invalid corpus files.
+ * If no mutator is used, returns false.
+ */
+ public static boolean invalidCorpusFilesPresent() {
+ return mutator != null && invalidCorpusFileWarningShown;
+ }
+
+ /**
+ * Disables libFuzzer's fuzz target exit detection until the next call to {@link #runOne}.
+ *
+ * <p>Calling {@link System#exit} after having called this method will not trigger libFuzzer's
+ * exit hook that would otherwise print the "fuzz target exited" error message. This method should
+ * thus only be called after control has returned from the user-provided fuzz target.
+ */
+ private static void temporarilyDisableLibfuzzerExitHook() {
+ FuzzTargetRunnerNatives.temporarilyDisableLibfuzzerExitHook();
+ }
+}