diff --git a/src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java b/src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java index d97f9f35d..f662eb59c 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java @@ -14,6 +14,9 @@ package com.code_intelligence.jazzer.junit; +import static com.code_intelligence.jazzer.junit.Utils.getClassPathBasedInstrumentationFilter; +import static com.code_intelligence.jazzer.junit.Utils.getLegacyInstrumentationFilter; + import java.io.File; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.extension.ExtensionContext; @@ -48,7 +51,11 @@ static void forFuzzing(ExtensionContext executionRequest) { String instrumentationFilter = executionRequest.getConfigurationParameter("jazzer.instrument") .orElseGet( - () -> Utils.defaultInstrumentationFilter(executionRequest.getRequiredTestClass())); + () + -> getClassPathBasedInstrumentationFilter(System.getProperty("java.class.path")) + .orElseGet(() + -> getLegacyInstrumentationFilter( + executionRequest.getRequiredTestClass()))); String filter = String.join(File.pathSeparator, instrumentationFilter.split(",")); System.setProperty("jazzer.custom_hook_includes", filter); System.setProperty("jazzer.instrumentation_includes", filter); diff --git a/src/main/java/com/code_intelligence/jazzer/junit/Utils.java b/src/main/java/com/code_intelligence/jazzer/junit/Utils.java index 45267ce63..4ab81cd15 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/Utils.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/Utils.java @@ -16,27 +16,35 @@ import static java.util.Arrays.stream; import static java.util.Collections.newSetFromMap; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; import static org.junit.jupiter.api.Named.named; import static org.junit.jupiter.params.provider.Arguments.arguments; import com.code_intelligence.jazzer.utils.UnsafeProvider; import com.code_intelligence.jazzer.utils.UnsafeUtils; +import java.io.File; +import java.io.IOException; import java.lang.invoke.MethodType; import java.lang.management.ManagementFactory; import java.lang.reflect.Array; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; +import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.time.Duration; import java.util.Arrays; +import java.util.HashSet; import java.util.IdentityHashMap; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ReflectiveInvocationContext; @@ -96,7 +104,10 @@ static Path generatedCorpusPath(Class testClass, Method testMethod) { return Paths.get(".cifuzz-corpus", testClass.getName(), testMethod.getName()); } - static String defaultInstrumentationFilter(Class testClass) { + /** + * Returns a heuristic default value for jazzer.instrument based on the test class. + */ + static String getLegacyInstrumentationFilter(Class testClass) { // This is an extremely rough "implementation" of the public suffix list algorithm // (https://publicsuffix.org/): It tries to guess the shortest prefix of the package name that // isn't public. It doesn't use the actual list, but instead assumes that every root segment as @@ -112,7 +123,71 @@ static String defaultInstrumentationFilter(Class testClass) { numSegments = 3; } return Stream.concat(Arrays.stream(packageSegments).limit(numSegments), Stream.of("**")) - .collect(Collectors.joining(".")); + .collect(joining(".")); + } + + private static final Pattern CLASSPATH_SPLITTER = + Pattern.compile(Pattern.quote(File.pathSeparator)); + + /** + * Returns a heuristic default value for jazzer.instrument based on the files on the provided + * classpath. + */ + static Optional getClassPathBasedInstrumentationFilter(String classPath) { + List includes = + CLASSPATH_SPLITTER.splitAsStream(classPath) + .map(Paths::get) + // We consider classpath entries that are directories rather than jar files to contain + // the classes of the current project rather than external dependencies. This is just a + // heuristic and breaks with build systems that package all classes in jar files, e.g. + // with Bazel. + .filter(Files::isDirectory) + .flatMap(root -> { + HashSet pkgs = new HashSet<>(); + try { + Files.walkFileTree(root, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory( + Path dir, BasicFileAttributes basicFileAttributes) throws IOException { + try (Stream entries = Files.list(dir)) { + // If a directory contains a .class file, we add an include filter matching it + // and all subdirectories. + // Special case: If there is a class defined at the root, only the unnamed + // package is included, so continue with the traversal of subdirectories + // to discover additional includes. + if (entries.filter(path -> path.toString().endsWith(".class")) + .anyMatch(Files::isRegularFile)) { + Path pkgPath = root.relativize(dir); + pkgs.add(pkgPath); + if (pkgPath.toString().isEmpty()) { + return FileVisitResult.CONTINUE; + } else { + return FileVisitResult.SKIP_SUBTREE; + } + } + } + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + // This is only a best-effort heuristic anyway, ignore this directory. + return Stream.of(); + } + return pkgs.stream(); + }) + .distinct() + .collect(toList()); + if (includes.isEmpty()) { + return Optional.empty(); + } + return Optional.of( + includes.stream() + .map(Path::toString) + // For classes without a package, only include the unnamed package. + .map(path -> path.isEmpty() ? "*" : path.replace(File.separator, ".") + ".**") + .sorted() + // jazzer.instrument uses ',' as the separator. + .collect(joining(","))); } private static final Pattern COVERAGE_AGENT_ARG = diff --git a/src/test/java/com/code_intelligence/jazzer/junit/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/junit/BUILD.bazel index 9492cb57a..dfcb2ec64 100644 --- a/src/test/java/com/code_intelligence/jazzer/junit/BUILD.bazel +++ b/src/test/java/com/code_intelligence/jazzer/junit/BUILD.bazel @@ -15,6 +15,7 @@ java_junit5_test( srcs = ["UtilsTest.java"], deps = JUNIT5_DEPS + [ "//src/main/java/com/code_intelligence/jazzer/junit:utils", + "@maven//:com_google_truth_extensions_truth_java8_extension", "@maven//:com_google_truth_truth", "@maven//:org_junit_jupiter_junit_jupiter_api", "@maven//:org_junit_jupiter_junit_jupiter_params", diff --git a/src/test/java/com/code_intelligence/jazzer/junit/UtilsTest.java b/src/test/java/com/code_intelligence/jazzer/junit/UtilsTest.java index eeb5efba1..da4a73459 100644 --- a/src/test/java/com/code_intelligence/jazzer/junit/UtilsTest.java +++ b/src/test/java/com/code_intelligence/jazzer/junit/UtilsTest.java @@ -20,11 +20,18 @@ import static com.code_intelligence.jazzer.junit.Utils.isMarkedInstance; import static com.code_intelligence.jazzer.junit.Utils.isMarkedInvocation; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static java.nio.file.Files.createDirectories; +import static java.nio.file.Files.createFile; import static java.util.Arrays.stream; import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.joining; import static org.junit.jupiter.params.provider.Arguments.arguments; +import java.io.File; +import java.io.IOException; import java.lang.reflect.Method; +import java.nio.file.Path; import java.util.AbstractList; import java.util.AbstractMap; import java.util.Arrays; @@ -38,12 +45,15 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.InvocationInterceptor; import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; public class UtilsTest implements InvocationInterceptor { + @TempDir Path temp; + @Test void testDurationStringToSeconds() { assertThat(durationStringToSeconds("1m")).isEqualTo(60); @@ -98,4 +108,44 @@ public void interceptTestTemplateMethod(Invocation invocation, argumentsExpectedToBeMarked = !argumentsExpectedToBeMarked; invocation.proceed(); } + + @Test + public void testGetClassPathBasedInstrumentationFilter() throws IOException { + Path firstDir = createDirectories(temp.resolve("first_dir")); + Path orgExample = createDirectories(firstDir.resolve("org").resolve("example")); + createFile(orgExample.resolve("Application.class")); + + Path nonExistentDir = temp.resolve("does not exist"); + + Path secondDir = createDirectories(temp.resolve("second").resolve("dir")); + createFile(secondDir.resolve("Root.class")); + Path comExampleProject = + createDirectories(secondDir.resolve("com").resolve("example").resolve("project")); + createFile(comExampleProject.resolve("Main.class")); + Path comExampleOtherProject = + createDirectories(secondDir.resolve("com").resolve("example").resolve("other_project")); + createFile(comExampleOtherProject.resolve("Lib.class")); + + Path emptyDir = createDirectories(temp.resolve("some").resolve("empty").resolve("dir")); + + Path firstJar = createFile(temp.resolve("first.jar")); + Path secondJar = createFile(temp.resolve("second.jar")); + + assertThat(Utils.getClassPathBasedInstrumentationFilter(makeClassPath( + firstDir, firstJar, nonExistentDir, secondDir, secondJar, emptyDir))) + .hasValue("*,com.example.other_project.**,com.example.project.**,org.example.**"); + } + + @Test + public void testGetClassPathBasedInstrumentationFilter_noDirs() throws IOException { + Path firstJar = createFile(temp.resolve("first.jar")); + Path secondJar = createFile(temp.resolve("second.jar")); + + assertThat(Utils.getClassPathBasedInstrumentationFilter(makeClassPath(firstJar, secondJar))) + .isEmpty(); + } + + private static String makeClassPath(Path... paths) { + return Arrays.stream(paths).map(Path::toString).collect(joining(File.pathSeparator)); + } }