• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2021 Code Intelligence GmbH
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 package com.code_intelligence.jazzer.tools;
15 
16 import com.google.devtools.build.runfiles.Runfiles;
17 import java.io.File;
18 import java.io.IOException;
19 import java.lang.reflect.InvocationTargetException;
20 import java.lang.reflect.Method;
21 import java.net.URL;
22 import java.net.URLClassLoader;
23 import java.nio.file.Files;
24 import java.nio.file.Path;
25 import java.nio.file.Paths;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Comparator;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Set;
32 import java.util.stream.Collectors;
33 import javax.tools.JavaCompiler;
34 import javax.tools.JavaCompiler.CompilationTask;
35 import javax.tools.JavaFileObject;
36 import javax.tools.StandardJavaFileManager;
37 import javax.tools.ToolProvider;
38 
39 public class FuzzTargetTestWrapper {
40   private static final boolean JAZZER_CI = "1".equals(System.getenv("JAZZER_CI"));
41 
main(String[] args)42   public static void main(String[] args) {
43     Runfiles runfiles;
44     String driverActualPath;
45     String apiActualPath;
46     String jarActualPath;
47     boolean verifyCrashInput;
48     boolean verifyCrashReproducer;
49     boolean expectCrash;
50     Set<String> expectedFindings;
51     List<String> arguments;
52     try {
53       runfiles = Runfiles.create();
54       driverActualPath = lookUpRunfile(runfiles, args[0]);
55       apiActualPath = lookUpRunfile(runfiles, args[1]);
56       jarActualPath = lookUpRunfile(runfiles, args[2]);
57       verifyCrashInput = Boolean.parseBoolean(args[3]);
58       verifyCrashReproducer = Boolean.parseBoolean(args[4]);
59       expectCrash = Boolean.parseBoolean(args[5]);
60       expectedFindings =
61           Arrays.stream(args[6].split(",")).filter(s -> !s.isEmpty()).collect(Collectors.toSet());
62       // Map all files/dirs to real location
63       arguments =
64           Arrays.stream(args)
65               .skip(7)
66               .map(arg -> arg.startsWith("-") ? arg : lookUpRunfileWithFallback(runfiles, arg))
67               .collect(Collectors.toList());
68     } catch (IOException | ArrayIndexOutOfBoundsException e) {
69       e.printStackTrace();
70       System.exit(1);
71       return;
72     }
73 
74     ProcessBuilder processBuilder = new ProcessBuilder();
75     Map<String, String> environment = processBuilder.environment();
76     // Ensure that Jazzer can find its runfiles.
77     environment.putAll(runfiles.getEnvVars());
78 
79     // Crashes will be available as test outputs. These are cleared on the next run,
80     // so this is only useful for examples.
81     String outputDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR");
82 
83     List<String> command = new ArrayList<>();
84     command.add(driverActualPath);
85     command.add(String.format("-artifact_prefix=%s/", outputDir));
86     command.add(String.format("--reproducer_path=%s", outputDir));
87     command.add(String.format("--cp=%s", jarActualPath));
88     if (System.getenv("JAZZER_NO_EXPLICIT_SEED") == null) {
89       command.add("-seed=2735196724");
90     }
91     command.addAll(arguments);
92 
93     processBuilder.inheritIO();
94     if (JAZZER_CI) {
95       // Make JVM error reports available in test outputs.
96       processBuilder.environment().put(
97           "JAVA_TOOL_OPTIONS", String.format("-XX:ErrorFile=%s/hs_err_pid%%p.log", outputDir));
98     }
99     processBuilder.command(command);
100 
101     try {
102       int exitCode = processBuilder.start().waitFor();
103       if (!expectCrash) {
104         if (exitCode != 0) {
105           System.err.printf(
106               "Did not expect a crash, but Jazzer exited with exit code %d%n", exitCode);
107           System.exit(1);
108         }
109         System.exit(0);
110       }
111       // Assert that we either found a crash in Java (exit code 77) or a sanitizer crash (exit code
112       // 76).
113       if (exitCode != 76 && exitCode != 77) {
114         System.err.printf("Did expect a crash, but Jazzer exited with exit code %d%n", exitCode);
115         System.exit(1);
116       }
117       String[] outputFiles = new File(outputDir).list();
118       if (outputFiles == null) {
119         System.err.printf("Jazzer did not write a crashing input into %s%n", outputDir);
120         System.exit(1);
121       }
122       // Verify that libFuzzer dumped a crashing input.
123       if (JAZZER_CI && verifyCrashInput
124           && Arrays.stream(outputFiles).noneMatch(name -> name.startsWith("crash-"))) {
125         System.err.printf("No crashing input found in %s%n", outputDir);
126         System.exit(1);
127       }
128       // Verify that libFuzzer dumped a crash reproducer.
129       if (JAZZER_CI && verifyCrashReproducer
130           && Arrays.stream(outputFiles).noneMatch(name -> name.startsWith("Crash_"))) {
131         System.err.printf("No crash reproducer found in %s%n", outputDir);
132         System.exit(1);
133       }
134     } catch (IOException | InterruptedException e) {
135       e.printStackTrace();
136       System.exit(1);
137     }
138 
139     if (JAZZER_CI && verifyCrashReproducer) {
140       try {
141         verifyCrashReproducer(
142             outputDir, driverActualPath, apiActualPath, jarActualPath, expectedFindings);
143       } catch (Exception e) {
144         e.printStackTrace();
145         System.exit(1);
146       }
147     }
148     System.exit(0);
149   }
150 
151   // Looks up a Bazel "rootpath" in this binary's runfiles and returns the resulting path.
lookUpRunfile(Runfiles runfiles, String rootpath)152   private static String lookUpRunfile(Runfiles runfiles, String rootpath) {
153     return runfiles.rlocation(rlocationPath(rootpath));
154   }
155 
156   // Looks up a Bazel "rootpath" in this binary's runfiles and returns the resulting path if it
157   // exists. If not, returns the original path unmodified.
lookUpRunfileWithFallback(Runfiles runfiles, String rootpath)158   private static String lookUpRunfileWithFallback(Runfiles runfiles, String rootpath) {
159     String candidatePath;
160     try {
161       candidatePath = lookUpRunfile(runfiles, rootpath);
162     } catch (IllegalArgumentException unused) {
163       // The argument to Runfiles.rlocation had an invalid format, which indicates that rootpath
164       // is not a Bazel "rootpath" but a user-supplied path that should be returned unchanged.
165       return rootpath;
166     }
167     if (new File(candidatePath).exists()) {
168       return candidatePath;
169     } else {
170       return rootpath;
171     }
172   }
173 
174   // Turns the result of Bazel's `$(rootpath ...)` into the correct format for rlocation.
rlocationPath(String rootpath)175   private static String rlocationPath(String rootpath) {
176     if (rootpath.startsWith("external/")) {
177       return rootpath.substring("external/".length());
178     } else {
179       return "jazzer/" + rootpath;
180     }
181   }
182 
verifyCrashReproducer(String outputDir, String driver, String api, String jar, Set<String> expectedFindings)183   private static void verifyCrashReproducer(String outputDir, String driver, String api, String jar,
184       Set<String> expectedFindings) throws Exception {
185     File source =
186         Files.list(Paths.get(outputDir))
187             .filter(f -> f.toFile().getName().endsWith(".java"))
188             // Verify the crash reproducer that was created last in order to reproduce the last
189             // crash when using --keep_going.
190             .max(Comparator.comparingLong(p -> p.toFile().lastModified()))
191             .map(Path::toFile)
192             .orElseThrow(
193                 () -> new IllegalStateException("Could not find crash reproducer in " + outputDir));
194     String crashReproducer = compile(source, driver, api, jar);
195     execute(crashReproducer, outputDir, expectedFindings);
196   }
197 
compile(File source, String driver, String api, String jar)198   private static String compile(File source, String driver, String api, String jar)
199       throws IOException {
200     JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
201     try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
202       Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(source);
203       List<String> options =
204           Arrays.asList("-classpath", String.join(File.pathSeparator, driver, api, jar));
205       System.out.printf(
206           "Compile crash reproducer %s with options %s%n", source.getAbsolutePath(), options);
207       CompilationTask task =
208           compiler.getTask(null, fileManager, null, options, null, compilationUnits);
209       if (!task.call()) {
210         throw new IllegalStateException("Could not compile crash reproducer " + source);
211       }
212       return source.getName().substring(0, source.getName().indexOf("."));
213     }
214   }
215 
execute(String classFile, String outputDir, Set<String> expectedFindings)216   private static void execute(String classFile, String outputDir, Set<String> expectedFindings)
217       throws IOException, ReflectiveOperationException {
218     try {
219       System.out.printf("Execute crash reproducer %s%n", classFile);
220       URLClassLoader classLoader =
221           new URLClassLoader(new URL[] {new URL("file://" + outputDir + "/")});
222       Class<?> crashReproducerClass = classLoader.loadClass(classFile);
223       Method main = crashReproducerClass.getMethod("main", String[].class);
224       System.setProperty("jazzer.is_reproducer", "true");
225       main.invoke(null, new Object[] {new String[] {}});
226       if (!expectedFindings.isEmpty()) {
227         throw new IllegalStateException("Expected crash with any of "
228             + String.join(", ", expectedFindings) + " not reproduced by " + classFile);
229       }
230       System.out.println("Reproducer finished successfully without finding");
231     } catch (InvocationTargetException e) {
232       // expect the invocation to fail with the prescribed finding
233       Throwable finding = e.getCause();
234       if (expectedFindings.isEmpty()) {
235         throw new IllegalStateException("Did not expect " + classFile + " to crash", finding);
236       } else if (expectedFindings.contains(finding.getClass().getName())) {
237         System.out.printf("Reproduced exception \"%s\"%n", finding.getMessage());
238       } else {
239         throw new IllegalStateException(
240             classFile + " did not crash with any of " + String.join(", ", expectedFindings),
241             finding);
242       }
243     }
244   }
245 }
246