• 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 static java.util.stream.Collectors.toList;
17 
18 import com.google.devtools.build.runfiles.AutoBazelRepository;
19 import com.google.devtools.build.runfiles.Runfiles;
20 import java.io.BufferedReader;
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InputStreamReader;
25 import java.lang.ProcessBuilder.Redirect;
26 import java.lang.reflect.InvocationTargetException;
27 import java.lang.reflect.Method;
28 import java.net.URL;
29 import java.net.URLClassLoader;
30 import java.nio.file.Files;
31 import java.nio.file.Path;
32 import java.nio.file.Paths;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collections;
36 import java.util.Comparator;
37 import java.util.List;
38 import java.util.Set;
39 import java.util.regex.Pattern;
40 import java.util.stream.Collectors;
41 import java.util.stream.Stream;
42 import javax.tools.JavaCompiler;
43 import javax.tools.JavaCompiler.CompilationTask;
44 import javax.tools.JavaFileObject;
45 import javax.tools.StandardJavaFileManager;
46 import javax.tools.ToolProvider;
47 
48 @AutoBazelRepository
49 public class FuzzTargetTestWrapper {
50   private static final String EXCEPTION_PREFIX = "== Java Exception: ";
51   private static final String FRAME_PREFIX = "\tat ";
52   private static final Pattern SANITIZER_FINDING = Pattern.compile("^SUMMARY: \\w*Sanitizer");
53   private static final String THREAD_DUMP_HEADER = "Stack traces of all JVM threads:";
54   private static final Set<String> PUBLIC_JAZZER_PACKAGES = Collections.unmodifiableSet(
55       Stream.of("api", "replay", "sanitizers").collect(Collectors.toSet()));
56 
main(String[] args)57   public static void main(String[] args) {
58     Runfiles runfiles;
59     Path driverActualPath;
60     Path apiActualPath;
61     Path targetJarActualPath;
62     Path hookJarActualPath;
63     boolean shouldVerifyCrashInput;
64     boolean shouldVerifyCrashReproducer;
65     boolean expectCrash;
66     boolean usesJavaLauncher;
67     Set<String> allowedFindings;
68     List<String> arguments;
69     try {
70       runfiles =
71           Runfiles.preload().withSourceRepository(AutoBazelRepository_FuzzTargetTestWrapper.NAME);
72       driverActualPath = Paths.get(runfiles.rlocation(args[0]));
73       apiActualPath = Paths.get(runfiles.rlocation(args[1]));
74       targetJarActualPath = Paths.get(runfiles.rlocation(args[2]));
75       hookJarActualPath = args[3].isEmpty() ? null : Paths.get(runfiles.rlocation(args[3]));
76       shouldVerifyCrashInput = Boolean.parseBoolean(args[4]);
77       shouldVerifyCrashReproducer = Boolean.parseBoolean(args[5]);
78       expectCrash = Boolean.parseBoolean(args[6]);
79       usesJavaLauncher = Boolean.parseBoolean(args[7]);
80       allowedFindings =
81           Arrays.stream(args[8].split(",")).filter(s -> !s.isEmpty()).collect(Collectors.toSet());
82       // Map all files/dirs to real location
83       arguments = Arrays.stream(args)
84                       .skip(9)
85                       .map(arg -> arg.startsWith("-") ? arg : runfiles.rlocation(arg))
86                       .collect(toList());
87     } catch (IOException | ArrayIndexOutOfBoundsException e) {
88       e.printStackTrace();
89       System.exit(1);
90       return;
91     }
92 
93     ProcessBuilder processBuilder = new ProcessBuilder();
94     // Ensure that Jazzer can find its runfiles.
95     processBuilder.environment().putAll(runfiles.getEnvVars());
96     // Ensure that sanitizers behave consistently across OSes and use a dedicated exit code to make
97     // them distinguishable from unexpected crashes.
98     processBuilder.environment().put("ASAN_OPTIONS", "abort_on_error=0:exitcode=76");
99     processBuilder.environment().put("UBSAN_OPTIONS", "abort_on_error=0:exitcode=76");
100 
101     // Crashes will be available as test outputs. These are cleared on the next run,
102     // so this is only useful for examples.
103     Path outputDir = Paths.get(System.getenv("TEST_UNDECLARED_OUTPUTS_DIR"));
104 
105     List<String> command = new ArrayList<>();
106     command.add(driverActualPath.toString());
107     if (usesJavaLauncher) {
108       if (hookJarActualPath != null) {
109         command.add(String.format("--main_advice_classpath=%s", hookJarActualPath));
110       }
111       if (System.getenv("JAZZER_DEBUG") != null) {
112         command.add("--debug");
113       }
114     } else {
115       command.add(String.format("--cp=%s",
116           hookJarActualPath == null
117               ? targetJarActualPath
118               : String.join(System.getProperty("path.separator"), targetJarActualPath.toString(),
119                   hookJarActualPath.toString())));
120     }
121     command.add(String.format("-artifact_prefix=%s/", outputDir));
122     command.add(String.format("--reproducer_path=%s", outputDir));
123     if (System.getenv("JAZZER_NO_EXPLICIT_SEED") == null) {
124       command.add("-seed=2735196724");
125     }
126     command.addAll(arguments);
127 
128     // Make JVM error reports available in test outputs.
129     processBuilder.environment().put(
130         "JAVA_TOOL_OPTIONS", String.format("-XX:ErrorFile=%s/hs_err_pid%%p.log", outputDir));
131     processBuilder.redirectOutput(Redirect.INHERIT);
132     processBuilder.redirectInput(Redirect.INHERIT);
133     processBuilder.command(command);
134 
135     try {
136       Process process = processBuilder.start();
137       try {
138         verifyFuzzerOutput(
139             process.getErrorStream(), allowedFindings, arguments.contains("--nohooks"));
140       } finally {
141         process.getErrorStream().close();
142       }
143       int exitCode = process.waitFor();
144       if (!expectCrash) {
145         if (exitCode != 0) {
146           System.err.printf(
147               "Did not expect a crash, but Jazzer exited with exit code %d%n", exitCode);
148           System.exit(1);
149         }
150         System.exit(0);
151       }
152       // Assert that we either found a crash in Java (exit code 77), a sanitizer crash (exit code
153       // 76), or a timeout (exit code 70).
154       if (exitCode != 76 && exitCode != 77
155           && !(allowedFindings.contains("timeout") && exitCode == 70)) {
156         System.err.printf("Did expect a crash, but Jazzer exited with exit code %d%n", exitCode);
157         System.exit(1);
158       }
159       List<Path> outputFiles = Files.list(outputDir).collect(toList());
160       if (outputFiles.isEmpty()) {
161         System.err.printf("Jazzer did not write a crashing input into %s%n", outputDir);
162         System.exit(1);
163       }
164       // Verify that libFuzzer dumped a crashing input.
165       if (shouldVerifyCrashInput
166           && outputFiles.stream().noneMatch(
167               name -> name.getFileName().toString().startsWith("crash-"))
168           && !(allowedFindings.contains("timeout")
169               && outputFiles.stream().anyMatch(
170                   name -> name.getFileName().toString().startsWith("timeout-")))) {
171         System.err.printf("No crashing input found in %s%n", outputDir);
172         System.exit(1);
173       }
174       // Verify that libFuzzer dumped a crash reproducer.
175       if (shouldVerifyCrashReproducer
176           && outputFiles.stream().noneMatch(
177               name -> name.getFileName().toString().startsWith("Crash_"))) {
178         System.err.printf("No crash reproducer found in %s%n", outputDir);
179         System.exit(1);
180       }
181     } catch (IOException | InterruptedException e) {
182       e.printStackTrace();
183       System.exit(1);
184     }
185 
186     if (shouldVerifyCrashReproducer) {
187       try {
188         verifyCrashReproducer(outputDir, apiActualPath, targetJarActualPath, allowedFindings);
189       } catch (Exception e) {
190         e.printStackTrace();
191         System.exit(1);
192       }
193     }
194     System.exit(0);
195   }
196 
verifyFuzzerOutput( InputStream fuzzerOutput, Set<String> expectedFindings, boolean noHooks)197   private static void verifyFuzzerOutput(
198       InputStream fuzzerOutput, Set<String> expectedFindings, boolean noHooks) throws IOException {
199     List<String> stackTrace;
200     try (BufferedReader reader = new BufferedReader(new InputStreamReader(fuzzerOutput))) {
201       stackTrace =
202           reader.lines()
203               .peek(System.err::println)
204               .filter(line
205                   -> line.startsWith(EXCEPTION_PREFIX) || line.startsWith(FRAME_PREFIX)
206                       || line.equals(THREAD_DUMP_HEADER) || SANITIZER_FINDING.matcher(line).find())
207               .collect(toList());
208     }
209     if (expectedFindings.isEmpty()) {
210       if (stackTrace.isEmpty()) {
211         return;
212       }
213       throw new IllegalStateException(String.format(
214           "Did not expect a finding, but got a stack trace:%n%s", String.join("\n", stackTrace)));
215     }
216     if (expectedFindings.contains("native")) {
217       // Expect a native sanitizer finding as well as a thread dump with at least one frame.
218       if (stackTrace.stream().noneMatch(line -> SANITIZER_FINDING.matcher(line).find())) {
219         throw new IllegalStateException("Expected native sanitizer finding, but did not get any");
220       }
221       if (!stackTrace.contains(THREAD_DUMP_HEADER) || stackTrace.size() < 3) {
222         throw new IllegalStateException(
223             "Expected stack traces for all threads, but did not get any");
224       }
225       if (expectedFindings.size() != 1) {
226         throw new IllegalStateException("Cannot expect both a native and other findings");
227       }
228       return;
229     }
230     if (expectedFindings.contains("timeout")) {
231       if (!stackTrace.contains(THREAD_DUMP_HEADER) || stackTrace.size() < 3) {
232         throw new IllegalStateException(
233             "Expected stack traces for all threads, but did not get any");
234       }
235       if (expectedFindings.size() != 1) {
236         throw new IllegalStateException("Cannot expect both a timeout and other findings");
237       }
238       return;
239     }
240     List<String> findings =
241         stackTrace.stream()
242             .filter(line -> line.startsWith(EXCEPTION_PREFIX))
243             .map(line -> line.substring(EXCEPTION_PREFIX.length()).split(":", 2)[0])
244             .collect(toList());
245     if (findings.isEmpty()) {
246       throw new IllegalStateException("Expected a crash, but did not get a stack trace");
247     }
248     for (String finding : findings) {
249       if (!expectedFindings.contains(finding)) {
250         throw new IllegalStateException(String.format("Got finding %s, but expected one of: %s",
251             findings.get(0), String.join(", ", expectedFindings)));
252       }
253     }
254     List<String> unexpectedFrames =
255         stackTrace.stream()
256             .filter(line -> line.startsWith(FRAME_PREFIX))
257             .map(line -> line.substring(FRAME_PREFIX.length()))
258             .filter(line -> line.startsWith("com.code_intelligence.jazzer."))
259             // With --nohooks, Jazzer does not filter out its own stack frames.
260             .filter(line
261                 -> !noHooks
262                     && !PUBLIC_JAZZER_PACKAGES.contains(
263                         line.substring("com.code_intelligence.jazzer.".length()).split("\\.")[0]))
264             .collect(toList());
265     if (!unexpectedFrames.isEmpty()) {
266       throw new IllegalStateException(
267           String.format("Unexpected strack trace frames:%n%n%s%n%nin:%n%s",
268               String.join("\n", unexpectedFrames), String.join("\n", stackTrace)));
269     }
270   }
271 
verifyCrashReproducer( Path outputDir, Path api, Path targetJar, Set<String> expectedFindings)272   private static void verifyCrashReproducer(
273       Path outputDir, Path api, Path targetJar, Set<String> expectedFindings) throws Exception {
274     File source =
275         Files.list(outputDir)
276             .filter(f -> f.toFile().getName().endsWith(".java"))
277             // Verify the crash reproducer that was created last in order to reproduce the last
278             // crash when using --keep_going.
279             .max(Comparator.comparingLong(p -> p.toFile().lastModified()))
280             .map(Path::toFile)
281             .orElseThrow(
282                 () -> new IllegalStateException("Could not find crash reproducer in " + outputDir));
283     String reproducerClassName = compile(source, api, targetJar);
284     execute(reproducerClassName, outputDir, api, targetJar, expectedFindings);
285   }
286 
compile(File source, Path api, Path targetJar)287   private static String compile(File source, Path api, Path targetJar) throws IOException {
288     JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
289     try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
290       Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(source);
291       List<String> options = Arrays.asList(
292           "-classpath", String.join(File.pathSeparator, api.toString(), targetJar.toString()));
293       System.out.printf(
294           "Compile crash reproducer %s with options %s%n", source.getAbsolutePath(), options);
295       CompilationTask task =
296           compiler.getTask(null, fileManager, null, options, null, compilationUnits);
297       if (!task.call()) {
298         throw new IllegalStateException("Could not compile crash reproducer " + source);
299       }
300       return source.getName().substring(0, source.getName().indexOf("."));
301     }
302   }
303 
execute(String className, Path outputDir, Path api, Path targetJar, Set<String> expectedFindings)304   private static void execute(String className, Path outputDir, Path api, Path targetJar,
305       Set<String> expectedFindings) throws IOException, ReflectiveOperationException {
306     try {
307       System.out.printf("Execute crash reproducer %s%n", className);
308       URLClassLoader classLoader = new URLClassLoader(
309           new URL[] {
310               outputDir.toUri().toURL(),
311               api.toUri().toURL(),
312               targetJar.toUri().toURL(),
313           },
314           getPlatformClassLoader());
315       Class<?> crashReproducerClass = classLoader.loadClass(className);
316       Method main = crashReproducerClass.getMethod("main", String[].class);
317       System.setProperty("jazzer.is_reproducer", "true");
318       main.invoke(null, new Object[] {new String[] {}});
319       if (!expectedFindings.isEmpty()) {
320         throw new IllegalStateException("Expected crash with any of "
321             + String.join(", ", expectedFindings) + " not reproduced by " + className);
322       }
323       System.out.println("Reproducer finished successfully without finding");
324     } catch (InvocationTargetException e) {
325       // expect the invocation to fail with the prescribed finding
326       Throwable finding = e.getCause();
327       if (expectedFindings.isEmpty()) {
328         throw new IllegalStateException("Did not expect " + className + " to crash", finding);
329       } else if (expectedFindings.contains(finding.getClass().getName())) {
330         System.out.printf("Reproduced exception \"%s\"%n", finding);
331       } else {
332         throw new IllegalStateException(
333             className + " did not crash with any of " + String.join(", ", expectedFindings),
334             finding);
335       }
336     }
337   }
338 
getPlatformClassLoader()339   private static ClassLoader getPlatformClassLoader() {
340     try {
341       Method getter = ClassLoader.class.getMethod("getPlatformClassLoader");
342       // Java 9 and higher
343       return (ClassLoader) getter.invoke(null);
344     } catch (NoSuchMethodException e) {
345       // Java 8: All standard library classes are visible through the ClassLoader represented by
346       // null.
347       return null;
348     } catch (InvocationTargetException | IllegalAccessException e) {
349       throw new RuntimeException(e);
350     }
351   }
352 }
353