blob: 1d05cdefa6beb724d51f43dccaa82aeec36af471 [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.junit;
import static com.code_intelligence.jazzer.utils.Utils.getReadableDescriptor;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.driver.FuzzTargetRunner;
import com.code_intelligence.jazzer.junit.JazzerTestEngine.JazzerFuzzTestDescriptor;
import com.code_intelligence.jazzer.junit.JazzerTestEngine.JazzerSetupError;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junit.platform.engine.ExecutionRequest;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.reporting.ReportEntry;
class JazzerFuzzTestExecutor {
private static final AtomicBoolean hasExecutedOnce = new AtomicBoolean();
private final ExecutionRequest request;
private final JazzerFuzzTestDescriptor fuzzTestDescriptor;
private final Path baseDir;
public JazzerFuzzTestExecutor(
ExecutionRequest request, JazzerFuzzTestDescriptor fuzzTestDescriptor, Path baseDir) {
this.request = request;
this.fuzzTestDescriptor = fuzzTestDescriptor;
this.baseDir = baseDir;
}
public TestExecutionResult execute() throws IOException, URISyntaxException {
if (!hasExecutedOnce.compareAndSet(false, true)) {
throw new IllegalStateException(
"Only a single fuzz test can be executed by JazzerFuzzTestExecutor per test run");
}
final Method fuzzTestMethod = fuzzTestDescriptor.getMethod();
final Class<?> fuzzTestClass = fuzzTestMethod.getDeclaringClass();
ArrayList<String> libFuzzerArgs = new ArrayList<>();
libFuzzerArgs.add("fake_argv0");
// Store the generated corpus in a per-class directory under the project root, just like cifuzz:
// https://github.com/CodeIntelligenceTesting/cifuzz/blob/bf410dcfbafbae2a73cf6c5fbed031cdfe234f2f/internal/cmd/run/run.go#L381
// The path is specified relative to the current working directory, which with JUnit is the
// project directory.
Path generatedCorpusDir = baseDir.resolve(Utils.generatedCorpusPath(fuzzTestClass));
Files.createDirectories(generatedCorpusDir);
libFuzzerArgs.add(generatedCorpusDir.toAbsolutePath().toString());
// If the default or configured seed corpus directory for the fuzz test exists as a regular
// directory on disk (i.e., the test is not run from a JAR), use it as a seeds directory for
// libFuzzer and also emit findings into it so that the regression test can be used to debug
// them.
FuzzTest fuzzTest = AnnotationSupport.findAnnotation(fuzzTestMethod, FuzzTest.class).get();
String seedCorpusResourcePath = fuzzTest.seedCorpus().isEmpty()
? Utils.defaultSeedCorpusPath(fuzzTestClass)
: fuzzTest.seedCorpus();
URL seedCorpusUrl = fuzzTestClass.getResource(seedCorpusResourcePath);
if (seedCorpusUrl == null) {
if (fuzzTest.seedCorpus().isEmpty()) {
// Situation: The user may not be aware of the seed corpus feature.
String message = String.format(
"Collecting crashing inputs in the project root directory.\nIf you want to keep them organized by "
+ "fuzz test and automatically run them as regression tests with JUnit Jupiter, create a "
+ "test resource directory called '%s' in package '%s' and move the files there.",
seedCorpusResourcePath, fuzzTestClass.getPackage().getName());
request.getEngineExecutionListener().reportingEntryPublished(
fuzzTestDescriptor, ReportEntry.from("seed corpus", message));
} else {
// Situation: The user explicitly configured a seed corpus, but it couldn't be found.
throw new FileNotFoundException(
String.format("Failed to find seed corpus at '%s' relative to '%s'",
fuzzTest.seedCorpus(), fuzzTestClass));
}
libFuzzerArgs.add(String.format("-artifact_prefix=%s%c", baseDir, File.separatorChar));
} else if ("file".equals(seedCorpusUrl.getProtocol())) {
// From the second positional argument on, files and directories are used as seeds but not
// modified. Using seedCorpusUrl.getFile() fails on Windows.
libFuzzerArgs.add(Paths.get(seedCorpusUrl.toURI()).toString());
// We try to find the source tree representation of the seed corpus directory and emit
// findings into it.
findSeedCorpusDirectoryInSourceTree().ifPresent(
(path)
-> libFuzzerArgs.add(
String.format("-artifact_prefix=%s%c", path, File.separatorChar)));
} else {
// We can't directly use the seed corpus from resources as it's packaged into a JAR. Instead,
// try to get the path to the seed corpus in the source tree.
Optional<Path> seedCorpusSourceDirectory = findSeedCorpusDirectoryInSourceTree();
if (seedCorpusSourceDirectory.isPresent()) {
libFuzzerArgs.add(seedCorpusSourceDirectory.get().toString());
// We try to find the source tree representation of the seed corpus directory and emit
// findings into it.
libFuzzerArgs.add(String.format(
"-artifact_prefix=%s%c", seedCorpusSourceDirectory.get(), File.separatorChar));
} else {
request.getEngineExecutionListener().reportingEntryPublished(fuzzTestDescriptor,
ReportEntry.from("seed corpus",
"When running Jazzer fuzz tests from a JAR rather than class files, the seed corpus isn't used unless it is located under src/test/resources/..."));
}
}
libFuzzerArgs.add("-max_total_time=" + durationStringToSeconds(fuzzTest.maxDuration()));
// Disable libFuzzer's out of memory detection: It is only useful for native library fuzzing,
// which we don't support without our native driver, and leads to false positives where it picks
// up IntelliJ's memory usage.
libFuzzerArgs.add("-rss_limit_mb=0");
if (request.getConfigurationParameters().getBoolean("jazzer.valueprofile").orElse(false)) {
libFuzzerArgs.add("-use_value_profile=1");
}
if (fuzzTestMethod.getParameterCount() == 0) {
return TestExecutionResult.failed(new JazzerSetupError(
"Methods annotated with @FuzzTest must take at least one parameter"));
}
if (fuzzTestMethod.getParameterCount() == 1
&& (fuzzTestMethod.getParameterTypes()[0] == byte[].class
|| fuzzTestMethod.getParameterTypes()[0] == FuzzedDataProvider.class)) {
System.setProperty("jazzer.target_class", fuzzTestClass.getName());
System.setProperty("jazzer.target_method", fuzzTestMethod.getName());
} else {
System.setProperty("jazzer.autofuzz",
String.format("%s::%s%s", fuzzTestClass.getName(), fuzzTestMethod.getName(),
getReadableDescriptor(fuzzTestMethod)));
}
AgentConfigurator.forFuzzing(request, fuzzTestClass);
AtomicReference<Throwable> atomicFinding = new AtomicReference<>();
FuzzTargetRunner.registerFindingHandler(t -> {
atomicFinding.set(t);
return false;
});
int exitCode = FuzzTargetRunner.startLibFuzzer(libFuzzerArgs);
Throwable finding = atomicFinding.get();
if (finding != null) {
return TestExecutionResult.failed(finding);
} else if (exitCode != 0) {
return TestExecutionResult.failed(
new JazzerSetupError("libFuzzer exited with exit code " + exitCode));
} else {
return TestExecutionResult.successful();
}
}
private static long durationStringToSeconds(String duration) {
// Convert the string to ISO 8601 (https://en.wikipedia.org/wiki/ISO_8601#Durations). We do not
// allow for duration
// units longer than hours, so we can always prepend PT.
String isoDuration = "PT" + duration.replace("min", "m").replace(" ", "");
return Duration.parse(isoDuration).getSeconds();
}
private Optional<Path> findSeedCorpusDirectoryInSourceTree() {
FuzzTest fuzzTest =
AnnotationSupport.findAnnotation(fuzzTestDescriptor.getMethod(), FuzzTest.class).get();
String seedCorpusResourcePath = fuzzTest.seedCorpus().isEmpty()
? Utils.defaultSeedCorpusPath(fuzzTestDescriptor.getMethod().getDeclaringClass())
: fuzzTest.seedCorpus();
// Make the seed corpus resource path absolute.
if (!seedCorpusResourcePath.startsWith("/")) {
String seedCorpusPackage =
fuzzTestDescriptor.getMethod().getDeclaringClass().getPackage().getName().replace(
'.', '/');
seedCorpusResourcePath = "/" + seedCorpusPackage + "/" + seedCorpusResourcePath;
}
// Following the Maven directory layout, we look up the seed corpus under src/test/resources.
// This should be correct also for multi-module projects as JUnit is usually launched in the
// current module's root directory.
Path sourceSeedCorpusPath = baseDir.resolve(
("src/test/resources" + seedCorpusResourcePath).replace('/', File.separatorChar));
if (Files.isDirectory(sourceSeedCorpusPath)) {
return Optional.of(sourceSeedCorpusPath);
} else {
return Optional.empty();
}
}
}