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