| /* |
| * 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 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 && Opt.isAndroid) { |
| 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) { |
| mutator.invoke(); |
| } 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. |
| private static int mutateOne(long data, int size, int maxSize, 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); |
| } |
| |
| ByteArrayOutputStream out = 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(out); |
| byte[] mutatedBytes = out.toByteArray(); |
| |
| int newSize = Math.min(mutatedBytes.length, maxSize); |
| UNSAFE.copyMemory(mutatedBytes, BYTE_ARRAY_OFFSET, null, data, 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"); |
| } |
| 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(); |
| } |
| } |