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