blob: da9006b10d162ed7143fa4678bce041245dbfdf9 [file] [log] [blame]
/*
* 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 static java.util.stream.Collectors.joining;
import com.code_intelligence.jazzer.agent.AgentInstaller;
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.FuzzTargetRunnerNatives;
import com.code_intelligence.jazzer.runtime.JazzerInternal;
import com.code_intelligence.jazzer.utils.UnsafeProvider;
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.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
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 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 {
AgentInstaller.install(Opt.hooks);
}
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);
// Default value of the libFuzzer -error_exitcode flag.
private static final int LIBFUZZER_ERROR_EXIT_CODE = 77;
// 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 final Set<Long> ignoredTokens = new HashSet<>(Opt.ignore);
private static final FuzzedDataProviderImpl fuzzedDataProvider =
FuzzedDataProviderImpl.withNativeData();
private static final Class<?> fuzzTargetClass;
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 ReproducerTemplate reproducerTemplate;
private static Predicate<Throwable> findingHandler;
static {
String targetClassName = FuzzTargetFinder.findFuzzTargetClassName();
if (targetClassName == null) {
err.println("Missing argument --target_class=<fuzz_target_class>");
exit(1);
throw new IllegalStateException("Not reached");
}
try {
FuzzTargetRunner.class.getClassLoader().setDefaultAssertionStatus(true);
fuzzTargetClass =
Class.forName(targetClassName, false, FuzzTargetRunner.class.getClassLoader());
} catch (ClassNotFoundException e) {
err.printf(
"ERROR: '%s' not found on classpath:%n%n%s%n%nAll required classes must be on the classpath specified via --cp.%n",
targetClassName, System.getProperty("java.class.path"));
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);
FuzzTargetFinder.FuzzTarget fuzzTarget;
try {
fuzzTarget = FuzzTargetFinder.findFuzzTarget(fuzzTargetClass);
} catch (IllegalArgumentException e) {
err.printf("ERROR: %s%n", e.getMessage());
exit(1);
throw new IllegalStateException("Not reached");
}
try {
fuzzTargetMethod = MethodHandles.lookup().unreflect(fuzzTarget.method);
} catch (IllegalAccessException e) {
// Should have been made accessible in FuzzTargetFinder.
throw new IllegalStateException(e);
}
useFuzzedDataProvider = fuzzTarget.useFuzzedDataProvider;
fuzzerTearDown = fuzzTarget.tearDown.orElse(null);
reproducerTemplate = new ReproducerTemplate(fuzzTargetClass.getName(), useFuzzedDataProvider);
try {
fuzzTargetInstance = fuzzTarget.newInstance.call();
} catch (Throwable e) {
err.print("== Java Exception during initialization: ");
e.printStackTrace(err);
exit(1);
throw new IllegalStateException("Not reached");
}
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 (useFuzzedDataProvider) {
fuzzedDataProvider.setNativeData(dataPtr, dataLength);
data = null;
argument = fuzzedDataProvider;
} else {
data = copyToArray(dataPtr, dataLength);
argument = data;
}
try {
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();
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.
// It doesn't support @FuzzTest fuzz targets, but these come with an integrated regression test
// that satisfies the same purpose.
if (fuzzTargetInstance == null) {
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) {
System.err.printf(
"%nNote: 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%n",
ignoredTokens.stream()
.map(token -> Long.toUnsignedString(token, 16))
.collect(joining(",")),
finding.getClass().getName());
}
System.exit(LIBFUZZER_ERROR_EXIT_CODE);
throw new IllegalStateException("Not reached");
}
return LIBFUZZER_CONTINUE;
}
/*
* 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));
}
/**
* 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;
}
err.println("calling fuzzerTearDown function");
try {
fuzzerTearDown.invoke(null);
} catch (InvocationTargetException e) {
// An exception in fuzzerTearDown is a regular finding.
err.print("== Java Exception in fuzzerTearDown: ");
e.getCause().printStackTrace(err);
System.exit(LIBFUZZER_ERROR_EXIT_CODE);
} catch (Throwable t) {
// Any other exception is an error.
t.printStackTrace(err);
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) {
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.
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);
}
/**
* Causes libFuzzer to write the current input to disk as a crashing input and emit some
* information about it to stderr.
*/
private static void printCrashingInput() {
FuzzTargetRunnerNatives.printCrashingInput();
}
/**
* 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();
}
}