diff options
Diffstat (limited to 'bazel/tools')
-rw-r--r-- | bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java | 291 | ||||
-rw-r--r-- | bazel/tools/java/com/code_intelligence/jazzer/tools/JarStripper.java | 86 |
2 files changed, 269 insertions, 108 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 index 107d8526..fe59c054 100644 --- a/bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java +++ b/bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java @@ -13,9 +13,16 @@ // limitations under the License. package com.code_intelligence.jazzer.tools; +import static java.util.stream.Collectors.toList; + +import com.google.devtools.build.runfiles.AutoBazelRepository; import com.google.devtools.build.runfiles.Runfiles; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.ProcessBuilder.Redirect; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; @@ -25,46 +32,58 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.tools.JavaCompiler; import javax.tools.JavaCompiler.CompilationTask; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; +@AutoBazelRepository public class FuzzTargetTestWrapper { - private static final boolean JAZZER_CI = "1".equals(System.getenv("JAZZER_CI")); + private static final String EXCEPTION_PREFIX = "== Java Exception: "; + private static final String FRAME_PREFIX = "\tat "; + private static final Pattern SANITIZER_FINDING = Pattern.compile("^SUMMARY: \\w*Sanitizer"); + private static final String THREAD_DUMP_HEADER = "Stack traces of all JVM threads:"; + private static final Set<String> PUBLIC_JAZZER_PACKAGES = Collections.unmodifiableSet( + Stream.of("api", "replay", "sanitizers").collect(Collectors.toSet())); public static void main(String[] args) { Runfiles runfiles; - String driverActualPath; - String apiActualPath; - String jarActualPath; - boolean verifyCrashInput; - boolean verifyCrashReproducer; + Path driverActualPath; + Path apiActualPath; + Path targetJarActualPath; + Path hookJarActualPath; + boolean shouldVerifyCrashInput; + boolean shouldVerifyCrashReproducer; boolean expectCrash; - Set<String> expectedFindings; + boolean usesJavaLauncher; + Set<String> allowedFindings; 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()); + runfiles = + Runfiles.preload().withSourceRepository(AutoBazelRepository_FuzzTargetTestWrapper.NAME); + driverActualPath = Paths.get(runfiles.rlocation(args[0])); + apiActualPath = Paths.get(runfiles.rlocation(args[1])); + targetJarActualPath = Paths.get(runfiles.rlocation(args[2])); + hookJarActualPath = args[3].isEmpty() ? null : Paths.get(runfiles.rlocation(args[3])); + shouldVerifyCrashInput = Boolean.parseBoolean(args[4]); + shouldVerifyCrashReproducer = Boolean.parseBoolean(args[5]); + expectCrash = Boolean.parseBoolean(args[6]); + usesJavaLauncher = Boolean.parseBoolean(args[7]); + allowedFindings = + Arrays.stream(args[8].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()); + arguments = Arrays.stream(args) + .skip(9) + .map(arg -> arg.startsWith("-") ? arg : runfiles.rlocation(arg)) + .collect(toList()); } catch (IOException | ArrayIndexOutOfBoundsException e) { e.printStackTrace(); System.exit(1); @@ -72,34 +91,56 @@ public class FuzzTargetTestWrapper { } ProcessBuilder processBuilder = new ProcessBuilder(); - Map<String, String> environment = processBuilder.environment(); // Ensure that Jazzer can find its runfiles. - environment.putAll(runfiles.getEnvVars()); + processBuilder.environment().putAll(runfiles.getEnvVars()); + // Ensure that sanitizers behave consistently across OSes and use a dedicated exit code to make + // them distinguishable from unexpected crashes. + processBuilder.environment().put("ASAN_OPTIONS", "abort_on_error=0:exitcode=76"); + processBuilder.environment().put("UBSAN_OPTIONS", "abort_on_error=0:exitcode=76"); // 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"); + Path outputDir = Paths.get(System.getenv("TEST_UNDECLARED_OUTPUTS_DIR")); List<String> command = new ArrayList<>(); - command.add(driverActualPath); + command.add(driverActualPath.toString()); + if (usesJavaLauncher) { + if (hookJarActualPath != null) { + command.add(String.format("--main_advice_classpath=%s", hookJarActualPath)); + } + if (System.getenv("JAZZER_DEBUG") != null) { + command.add("--debug"); + } + } else { + command.add(String.format("--cp=%s", + hookJarActualPath == null + ? targetJarActualPath + : String.join(System.getProperty("path.separator"), targetJarActualPath.toString(), + hookJarActualPath.toString()))); + } 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)); - } + // 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.redirectOutput(Redirect.INHERIT); + processBuilder.redirectInput(Redirect.INHERIT); processBuilder.command(command); try { - int exitCode = processBuilder.start().waitFor(); + Process process = processBuilder.start(); + try { + verifyFuzzerOutput( + process.getErrorStream(), allowedFindings, arguments.contains("--nohooks")); + } finally { + process.getErrorStream().close(); + } + int exitCode = process.waitFor(); if (!expectCrash) { if (exitCode != 0) { System.err.printf( @@ -108,26 +149,32 @@ public class FuzzTargetTestWrapper { } 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) { + // Assert that we either found a crash in Java (exit code 77), a sanitizer crash (exit code + // 76), or a timeout (exit code 70). + if (exitCode != 76 && exitCode != 77 + && !(allowedFindings.contains("timeout") && exitCode == 70)) { 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) { + List<Path> outputFiles = Files.list(outputDir).collect(toList()); + if (outputFiles.isEmpty()) { 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-"))) { + if (shouldVerifyCrashInput + && outputFiles.stream().noneMatch( + name -> name.getFileName().toString().startsWith("crash-")) + && !(allowedFindings.contains("timeout") + && outputFiles.stream().anyMatch( + name -> name.getFileName().toString().startsWith("timeout-")))) { 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_"))) { + if (shouldVerifyCrashReproducer + && outputFiles.stream().noneMatch( + name -> name.getFileName().toString().startsWith("Crash_"))) { System.err.printf("No crash reproducer found in %s%n", outputDir); System.exit(1); } @@ -136,10 +183,9 @@ public class FuzzTargetTestWrapper { System.exit(1); } - if (JAZZER_CI && verifyCrashReproducer) { + if (shouldVerifyCrashReproducer) { try { - verifyCrashReproducer( - outputDir, driverActualPath, apiActualPath, jarActualPath, expectedFindings); + verifyCrashReproducer(outputDir, apiActualPath, targetJarActualPath, allowedFindings); } catch (Exception e) { e.printStackTrace(); System.exit(1); @@ -148,42 +194,85 @@ public class FuzzTargetTestWrapper { 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; + private static void verifyFuzzerOutput( + InputStream fuzzerOutput, Set<String> expectedFindings, boolean noHooks) throws IOException { + List<String> stackTrace; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(fuzzerOutput))) { + stackTrace = + reader.lines() + .peek(System.err::println) + .filter(line + -> line.startsWith(EXCEPTION_PREFIX) || line.startsWith(FRAME_PREFIX) + || line.equals(THREAD_DUMP_HEADER) || SANITIZER_FINDING.matcher(line).find()) + .collect(toList()); } - if (new File(candidatePath).exists()) { - return candidatePath; - } else { - return rootpath; + if (expectedFindings.isEmpty()) { + if (stackTrace.isEmpty()) { + return; + } + throw new IllegalStateException(String.format( + "Did not expect a finding, but got a stack trace:%n%s", String.join("\n", stackTrace))); } - } - - // 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; + if (expectedFindings.contains("native")) { + // Expect a native sanitizer finding as well as a thread dump with at least one frame. + if (stackTrace.stream().noneMatch(line -> SANITIZER_FINDING.matcher(line).find())) { + throw new IllegalStateException("Expected native sanitizer finding, but did not get any"); + } + if (!stackTrace.contains(THREAD_DUMP_HEADER) || stackTrace.size() < 3) { + throw new IllegalStateException( + "Expected stack traces for all threads, but did not get any"); + } + if (expectedFindings.size() != 1) { + throw new IllegalStateException("Cannot expect both a native and other findings"); + } + return; + } + if (expectedFindings.contains("timeout")) { + if (!stackTrace.contains(THREAD_DUMP_HEADER) || stackTrace.size() < 3) { + throw new IllegalStateException( + "Expected stack traces for all threads, but did not get any"); + } + if (expectedFindings.size() != 1) { + throw new IllegalStateException("Cannot expect both a timeout and other findings"); + } + return; + } + List<String> findings = + stackTrace.stream() + .filter(line -> line.startsWith(EXCEPTION_PREFIX)) + .map(line -> line.substring(EXCEPTION_PREFIX.length()).split(":", 2)[0]) + .collect(toList()); + if (findings.isEmpty()) { + throw new IllegalStateException("Expected a crash, but did not get a stack trace"); + } + for (String finding : findings) { + if (!expectedFindings.contains(finding)) { + throw new IllegalStateException(String.format("Got finding %s, but expected one of: %s", + findings.get(0), String.join(", ", expectedFindings))); + } + } + List<String> unexpectedFrames = + stackTrace.stream() + .filter(line -> line.startsWith(FRAME_PREFIX)) + .map(line -> line.substring(FRAME_PREFIX.length())) + .filter(line -> line.startsWith("com.code_intelligence.jazzer.")) + // With --nohooks, Jazzer does not filter out its own stack frames. + .filter(line + -> !noHooks + && !PUBLIC_JAZZER_PACKAGES.contains( + line.substring("com.code_intelligence.jazzer.".length()).split("\\.")[0])) + .collect(toList()); + if (!unexpectedFrames.isEmpty()) { + throw new IllegalStateException( + String.format("Unexpected strack trace frames:%n%n%s%n%nin:%n%s", + String.join("\n", unexpectedFrames), String.join("\n", stackTrace))); } } - private static void verifyCrashReproducer(String outputDir, String driver, String api, String jar, - Set<String> expectedFindings) throws Exception { + private static void verifyCrashReproducer( + Path outputDir, Path api, Path targetJar, Set<String> expectedFindings) throws Exception { File source = - Files.list(Paths.get(outputDir)) + Files.list(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. @@ -191,17 +280,16 @@ public class FuzzTargetTestWrapper { .map(Path::toFile) .orElseThrow( () -> new IllegalStateException("Could not find crash reproducer in " + outputDir)); - String crashReproducer = compile(source, driver, api, jar); - execute(crashReproducer, outputDir, expectedFindings); + String reproducerClassName = compile(source, api, targetJar); + execute(reproducerClassName, outputDir, api, targetJar, expectedFindings); } - private static String compile(File source, String driver, String api, String jar) - throws IOException { + private static String compile(File source, Path api, Path targetJar) 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)); + List<String> options = Arrays.asList( + "-classpath", String.join(File.pathSeparator, api.toString(), targetJar.toString())); System.out.printf( "Compile crash reproducer %s with options %s%n", source.getAbsolutePath(), options); CompilationTask task = @@ -213,33 +301,52 @@ public class FuzzTargetTestWrapper { } } - private static void execute(String classFile, String outputDir, Set<String> expectedFindings) - throws IOException, ReflectiveOperationException { + private static void execute(String className, Path outputDir, Path api, Path targetJar, + 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); + System.out.printf("Execute crash reproducer %s%n", className); + URLClassLoader classLoader = new URLClassLoader( + new URL[] { + outputDir.toUri().toURL(), + api.toUri().toURL(), + targetJar.toUri().toURL(), + }, + getPlatformClassLoader()); + Class<?> crashReproducerClass = classLoader.loadClass(className); 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); + + String.join(", ", expectedFindings) + " not reproduced by " + className); } 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); + throw new IllegalStateException("Did not expect " + className + " to crash", finding); } else if (expectedFindings.contains(finding.getClass().getName())) { - System.out.printf("Reproduced exception \"%s\"%n", finding.getMessage()); + System.out.printf("Reproduced exception \"%s\"%n", finding); } else { throw new IllegalStateException( - classFile + " did not crash with any of " + String.join(", ", expectedFindings), + className + " did not crash with any of " + String.join(", ", expectedFindings), finding); } } } + + private static ClassLoader getPlatformClassLoader() { + try { + Method getter = ClassLoader.class.getMethod("getPlatformClassLoader"); + // Java 9 and higher + return (ClassLoader) getter.invoke(null); + } catch (NoSuchMethodException e) { + // Java 8: All standard library classes are visible through the ClassLoader represented by + // null. + return null; + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } } diff --git a/bazel/tools/java/com/code_intelligence/jazzer/tools/JarStripper.java b/bazel/tools/java/com/code_intelligence/jazzer/tools/JarStripper.java index 2a567c68..72f53cd7 100644 --- a/bazel/tools/java/com/code_intelligence/jazzer/tools/JarStripper.java +++ b/bazel/tools/java/com/code_intelligence/jazzer/tools/JarStripper.java @@ -14,21 +14,30 @@ package com.code_intelligence.jazzer.tools; +import static java.util.Collections.unmodifiableMap; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.partitioningBy; +import static java.util.stream.Collectors.toList; + import java.io.IOException; +import java.io.UncheckedIOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.PathMatcher; import java.nio.file.Paths; +import java.util.AbstractMap.SimpleEntry; import java.util.Arrays; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.TimeZone; -import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; public class JarStripper { @@ -43,14 +52,23 @@ public class JarStripper { if (args.length < 2) { System.err.println( "Hermetically removes files and directories from .jar files by relative paths."); - System.err.println("Usage: in.jar out.jar [relative path]..."); + System.err.println("Usage: in.jar out.jar [[+]path]..."); System.exit(1); } Path inFile = Paths.get(args[0]); Path outFile = Paths.get(args[1]); - Iterable<String> pathsToDelete = - Collections.unmodifiableList(Arrays.stream(args).skip(2).collect(Collectors.toList())); + Map<Boolean, List<String>> rawPaths = unmodifiableMap( + Arrays.stream(args) + .skip(2) + .map(arg -> { + if (arg.startsWith("+")) { + return new SimpleEntry<>(true, arg.substring(1)); + } else { + return new SimpleEntry<>(false, arg); + } + }) + .collect(partitioningBy(e -> e.getKey(), mapping(e -> e.getValue(), toList())))); try { Files.copy(inFile, outFile); @@ -76,19 +94,55 @@ public class JarStripper { TimeZone.setDefault(TimeZone.getTimeZone("UTC")); try (FileSystem zipFs = FileSystems.newFileSystem(outUri, ZIP_FS_PROPERTIES)) { - for (String pathToDelete : pathsToDelete) { - // Visit files before the directory they are contained in by sorting in reverse order. - try (Stream<Path> walk = Files.walk(zipFs.getPath(pathToDelete))) { - Iterable<Path> subpaths = - walk.sorted(Comparator.reverseOrder()).collect(Collectors.toList()); - for (Path subpath : subpaths) { - Files.delete(subpath); - } - } + PathMatcher pathsToDelete = toPathMatcher(zipFs, rawPaths.get(false), false); + PathMatcher pathsToKeep = toPathMatcher(zipFs, rawPaths.get(true), true); + try (Stream<Path> walk = Files.walk(zipFs.getPath(""))) { + walk.sorted(Comparator.reverseOrder()) + .filter(path + -> (pathsToKeep != null && !pathsToKeep.matches(path)) + || (pathsToDelete != null && pathsToDelete.matches(path))) + .forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); } - } catch (IOException e) { - e.printStackTrace(); + } catch (Throwable e) { + Throwable throwable = e; + if (throwable instanceof UncheckedIOException) { + throwable = throwable.getCause(); + } + throwable.printStackTrace(); System.exit(1); } } + + private static PathMatcher toPathMatcher(FileSystem fs, List<String> paths, boolean keep) { + if (paths.isEmpty()) { + return null; + } + return fs.getPathMatcher(String.format("glob:{%s}", + paths.stream() + .flatMap(pattern -> keep ? toKeepGlobs(pattern) : toRemoveGlobs(pattern)) + .collect(joining(",")))); + } + + private static Stream<String> toRemoveGlobs(String path) { + if (path.endsWith("/**")) { + // When removing all contents of a directory, also remove the directory itself. + return Stream.of(path, path.substring(0, path.length() - "/**".length())); + } else { + return Stream.of(path); + } + } + + private static Stream<String> toKeepGlobs(String path) { + // When keeping something, also keep all parents. + String[] segments = path.split("/"); + return Stream.concat(Stream.of(path), + IntStream.range(0, segments.length) + .mapToObj(i -> Arrays.stream(segments).limit(i).collect(joining("/")))); + } } |