aboutsummaryrefslogtreecommitdiff
path: root/bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java
diff options
context:
space:
mode:
Diffstat (limited to 'bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java')
-rw-r--r--bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java245
1 files changed, 245 insertions, 0 deletions
diff --git a/bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java b/bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java
new file mode 100644
index 00000000..107d8526
--- /dev/null
+++ b/bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java
@@ -0,0 +1,245 @@
+// Copyright 2021 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.tools;
+
+import com.google.devtools.build.runfiles.Runfiles;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaCompiler.CompilationTask;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.ToolProvider;
+
+public class FuzzTargetTestWrapper {
+ private static final boolean JAZZER_CI = "1".equals(System.getenv("JAZZER_CI"));
+
+ public static void main(String[] args) {
+ Runfiles runfiles;
+ String driverActualPath;
+ String apiActualPath;
+ String jarActualPath;
+ boolean verifyCrashInput;
+ boolean verifyCrashReproducer;
+ boolean expectCrash;
+ Set<String> expectedFindings;
+ List<String> arguments;
+ try {
+ runfiles = Runfiles.create();
+ driverActualPath = lookUpRunfile(runfiles, args[0]);
+ apiActualPath = lookUpRunfile(runfiles, args[1]);
+ jarActualPath = lookUpRunfile(runfiles, args[2]);
+ verifyCrashInput = Boolean.parseBoolean(args[3]);
+ verifyCrashReproducer = Boolean.parseBoolean(args[4]);
+ expectCrash = Boolean.parseBoolean(args[5]);
+ expectedFindings =
+ Arrays.stream(args[6].split(",")).filter(s -> !s.isEmpty()).collect(Collectors.toSet());
+ // Map all files/dirs to real location
+ arguments =
+ Arrays.stream(args)
+ .skip(7)
+ .map(arg -> arg.startsWith("-") ? arg : lookUpRunfileWithFallback(runfiles, arg))
+ .collect(Collectors.toList());
+ } catch (IOException | ArrayIndexOutOfBoundsException e) {
+ e.printStackTrace();
+ System.exit(1);
+ return;
+ }
+
+ ProcessBuilder processBuilder = new ProcessBuilder();
+ Map<String, String> environment = processBuilder.environment();
+ // Ensure that Jazzer can find its runfiles.
+ environment.putAll(runfiles.getEnvVars());
+
+ // Crashes will be available as test outputs. These are cleared on the next run,
+ // so this is only useful for examples.
+ String outputDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR");
+
+ List<String> command = new ArrayList<>();
+ command.add(driverActualPath);
+ command.add(String.format("-artifact_prefix=%s/", outputDir));
+ command.add(String.format("--reproducer_path=%s", outputDir));
+ command.add(String.format("--cp=%s", jarActualPath));
+ if (System.getenv("JAZZER_NO_EXPLICIT_SEED") == null) {
+ command.add("-seed=2735196724");
+ }
+ command.addAll(arguments);
+
+ processBuilder.inheritIO();
+ if (JAZZER_CI) {
+ // Make JVM error reports available in test outputs.
+ processBuilder.environment().put(
+ "JAVA_TOOL_OPTIONS", String.format("-XX:ErrorFile=%s/hs_err_pid%%p.log", outputDir));
+ }
+ processBuilder.command(command);
+
+ try {
+ int exitCode = processBuilder.start().waitFor();
+ if (!expectCrash) {
+ if (exitCode != 0) {
+ System.err.printf(
+ "Did not expect a crash, but Jazzer exited with exit code %d%n", exitCode);
+ System.exit(1);
+ }
+ System.exit(0);
+ }
+ // Assert that we either found a crash in Java (exit code 77) or a sanitizer crash (exit code
+ // 76).
+ if (exitCode != 76 && exitCode != 77) {
+ System.err.printf("Did expect a crash, but Jazzer exited with exit code %d%n", exitCode);
+ System.exit(1);
+ }
+ String[] outputFiles = new File(outputDir).list();
+ if (outputFiles == null) {
+ System.err.printf("Jazzer did not write a crashing input into %s%n", outputDir);
+ System.exit(1);
+ }
+ // Verify that libFuzzer dumped a crashing input.
+ if (JAZZER_CI && verifyCrashInput
+ && Arrays.stream(outputFiles).noneMatch(name -> name.startsWith("crash-"))) {
+ System.err.printf("No crashing input found in %s%n", outputDir);
+ System.exit(1);
+ }
+ // Verify that libFuzzer dumped a crash reproducer.
+ if (JAZZER_CI && verifyCrashReproducer
+ && Arrays.stream(outputFiles).noneMatch(name -> name.startsWith("Crash_"))) {
+ System.err.printf("No crash reproducer found in %s%n", outputDir);
+ System.exit(1);
+ }
+ } catch (IOException | InterruptedException e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+
+ if (JAZZER_CI && verifyCrashReproducer) {
+ try {
+ verifyCrashReproducer(
+ outputDir, driverActualPath, apiActualPath, jarActualPath, expectedFindings);
+ } catch (Exception e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+ System.exit(0);
+ }
+
+ // Looks up a Bazel "rootpath" in this binary's runfiles and returns the resulting path.
+ private static String lookUpRunfile(Runfiles runfiles, String rootpath) {
+ return runfiles.rlocation(rlocationPath(rootpath));
+ }
+
+ // Looks up a Bazel "rootpath" in this binary's runfiles and returns the resulting path if it
+ // exists. If not, returns the original path unmodified.
+ private static String lookUpRunfileWithFallback(Runfiles runfiles, String rootpath) {
+ String candidatePath;
+ try {
+ candidatePath = lookUpRunfile(runfiles, rootpath);
+ } catch (IllegalArgumentException unused) {
+ // The argument to Runfiles.rlocation had an invalid format, which indicates that rootpath
+ // is not a Bazel "rootpath" but a user-supplied path that should be returned unchanged.
+ return rootpath;
+ }
+ if (new File(candidatePath).exists()) {
+ return candidatePath;
+ } else {
+ return rootpath;
+ }
+ }
+
+ // Turns the result of Bazel's `$(rootpath ...)` into the correct format for rlocation.
+ private static String rlocationPath(String rootpath) {
+ if (rootpath.startsWith("external/")) {
+ return rootpath.substring("external/".length());
+ } else {
+ return "jazzer/" + rootpath;
+ }
+ }
+
+ private static void verifyCrashReproducer(String outputDir, String driver, String api, String jar,
+ Set<String> expectedFindings) throws Exception {
+ File source =
+ Files.list(Paths.get(outputDir))
+ .filter(f -> f.toFile().getName().endsWith(".java"))
+ // Verify the crash reproducer that was created last in order to reproduce the last
+ // crash when using --keep_going.
+ .max(Comparator.comparingLong(p -> p.toFile().lastModified()))
+ .map(Path::toFile)
+ .orElseThrow(
+ () -> new IllegalStateException("Could not find crash reproducer in " + outputDir));
+ String crashReproducer = compile(source, driver, api, jar);
+ execute(crashReproducer, outputDir, expectedFindings);
+ }
+
+ private static String compile(File source, String driver, String api, String jar)
+ throws IOException {
+ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+ try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
+ Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(source);
+ List<String> options =
+ Arrays.asList("-classpath", String.join(File.pathSeparator, driver, api, jar));
+ System.out.printf(
+ "Compile crash reproducer %s with options %s%n", source.getAbsolutePath(), options);
+ CompilationTask task =
+ compiler.getTask(null, fileManager, null, options, null, compilationUnits);
+ if (!task.call()) {
+ throw new IllegalStateException("Could not compile crash reproducer " + source);
+ }
+ return source.getName().substring(0, source.getName().indexOf("."));
+ }
+ }
+
+ private static void execute(String classFile, String outputDir, Set<String> expectedFindings)
+ throws IOException, ReflectiveOperationException {
+ try {
+ System.out.printf("Execute crash reproducer %s%n", classFile);
+ URLClassLoader classLoader =
+ new URLClassLoader(new URL[] {new URL("file://" + outputDir + "/")});
+ Class<?> crashReproducerClass = classLoader.loadClass(classFile);
+ Method main = crashReproducerClass.getMethod("main", String[].class);
+ System.setProperty("jazzer.is_reproducer", "true");
+ main.invoke(null, new Object[] {new String[] {}});
+ if (!expectedFindings.isEmpty()) {
+ throw new IllegalStateException("Expected crash with any of "
+ + String.join(", ", expectedFindings) + " not reproduced by " + classFile);
+ }
+ System.out.println("Reproducer finished successfully without finding");
+ } catch (InvocationTargetException e) {
+ // expect the invocation to fail with the prescribed finding
+ Throwable finding = e.getCause();
+ if (expectedFindings.isEmpty()) {
+ throw new IllegalStateException("Did not expect " + classFile + " to crash", finding);
+ } else if (expectedFindings.contains(finding.getClass().getName())) {
+ System.out.printf("Reproduced exception \"%s\"%n", finding.getMessage());
+ } else {
+ throw new IllegalStateException(
+ classFile + " did not crash with any of " + String.join(", ", expectedFindings),
+ finding);
+ }
+ }
+ }
+}