From 6f5b3c5e6e5c11c36db68ece32059ed56853815a Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Wed, 26 Apr 2023 17:39:54 +0200 Subject: [PATCH] junit: Compute a default for `jazzer.instrument` based on the class path The new heuristic walks the classpath directory and considers all directories (but not jar files) to constitute the classes of the current project. All subdirectories that include `.class` files are translated into packages and all their subpackages are subject to instrumentation. This is expected to give much better results than the previous heuristic, which only considered the first two or three segments of the test class package. --- .../jazzer/junit/AgentConfigurator.java | 9 ++- .../code_intelligence/jazzer/junit/Utils.java | 81 ++++++++++++++++++- .../jazzer/junit/BUILD.bazel | 1 + .../jazzer/junit/UtilsTest.java | 50 ++++++++++++ 4 files changed, 137 insertions(+), 4 deletions(-) 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)); + } }