1 // Copyright 2022 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 15 package com.code_intelligence.jazzer.junit; 16 17 import static java.util.Arrays.stream; 18 import static java.util.Collections.newSetFromMap; 19 import static java.util.stream.Collectors.joining; 20 import static java.util.stream.Collectors.toList; 21 import static org.junit.jupiter.api.Named.named; 22 import static org.junit.jupiter.params.provider.Arguments.arguments; 23 24 import com.code_intelligence.jazzer.utils.UnsafeProvider; 25 import com.code_intelligence.jazzer.utils.UnsafeUtils; 26 import java.io.File; 27 import java.io.IOException; 28 import java.lang.invoke.MethodType; 29 import java.lang.management.ManagementFactory; 30 import java.lang.reflect.Array; 31 import java.lang.reflect.Method; 32 import java.lang.reflect.Modifier; 33 import java.lang.reflect.Proxy; 34 import java.nio.file.FileVisitResult; 35 import java.nio.file.Files; 36 import java.nio.file.Path; 37 import java.nio.file.Paths; 38 import java.nio.file.SimpleFileVisitor; 39 import java.nio.file.attribute.BasicFileAttributes; 40 import java.time.Duration; 41 import java.util.Arrays; 42 import java.util.HashSet; 43 import java.util.IdentityHashMap; 44 import java.util.List; 45 import java.util.Optional; 46 import java.util.Set; 47 import java.util.regex.Pattern; 48 import java.util.stream.Stream; 49 import org.junit.jupiter.api.extension.ExtensionContext; 50 import org.junit.jupiter.api.extension.ReflectiveInvocationContext; 51 import org.junit.jupiter.params.provider.Arguments; 52 53 class Utils { 54 /** 55 * Returns the resource path of the inputs directory for a given test class and method. The path 56 * will have the form 57 * {@code <class name>Inputs/<method name>} 58 */ inputsDirectoryResourcePath(Class<?> testClass, Method testMethod)59 static String inputsDirectoryResourcePath(Class<?> testClass, Method testMethod) { 60 return testClass.getSimpleName() + "Inputs" 61 + "/" + testMethod.getName(); 62 } 63 inputsDirectoryResourcePath(Class<?> testClass)64 static String inputsDirectoryResourcePath(Class<?> testClass) { 65 return testClass.getSimpleName() + "Inputs"; 66 } 67 68 /** 69 * Returns the file system path of the inputs corpus directory in the source tree, if it exists. 70 * The directory is created if it does not exist, but the test resource directory itself exists. 71 */ inputsDirectorySourcePath( Class<?> testClass, Method testMethod, Path baseDir)72 static Optional<Path> inputsDirectorySourcePath( 73 Class<?> testClass, Method testMethod, Path baseDir) { 74 String inputsResourcePath = Utils.inputsDirectoryResourcePath(testClass, testMethod); 75 // Make the inputs resource path absolute. 76 if (!inputsResourcePath.startsWith("/")) { 77 String inputsPackage = testClass.getPackage().getName().replace('.', '/'); 78 inputsResourcePath = "/" + inputsPackage + "/" + inputsResourcePath; 79 } 80 81 // Following the Maven directory layout, we look up the inputs directory under 82 // src/test/resources. This should be correct also for multi-module projects as JUnit is usually 83 // launched in the current module's root directory. 84 Path testResourcesDirectory = baseDir.resolve("src").resolve("test").resolve("resources"); 85 Path sourceInputsDirectory = testResourcesDirectory; 86 for (String segment : inputsResourcePath.split("/")) { 87 sourceInputsDirectory = sourceInputsDirectory.resolve(segment); 88 } 89 if (Files.isDirectory(sourceInputsDirectory)) { 90 return Optional.of(sourceInputsDirectory); 91 } 92 // If we can at least find the test resource directory, create the inputs directory. 93 if (!Files.isDirectory(testResourcesDirectory)) { 94 return Optional.empty(); 95 } 96 try { 97 return Optional.of(Files.createDirectories(sourceInputsDirectory)); 98 } catch (Exception e) { 99 return Optional.empty(); 100 } 101 } 102 generatedCorpusPath(Class<?> testClass, Method testMethod)103 static Path generatedCorpusPath(Class<?> testClass, Method testMethod) { 104 return Paths.get(".cifuzz-corpus", testClass.getName(), testMethod.getName()); 105 } 106 107 /** 108 * Returns a heuristic default value for jazzer.instrument based on the test class. 109 */ getLegacyInstrumentationFilter(Class<?> testClass)110 static String getLegacyInstrumentationFilter(Class<?> testClass) { 111 // This is an extremely rough "implementation" of the public suffix list algorithm 112 // (https://publicsuffix.org/): It tries to guess the shortest prefix of the package name that 113 // isn't public. It doesn't use the actual list, but instead assumes that every root segment as 114 // well as "com.github" are public. Examples: 115 // - com.example.Test --> com.example.** 116 // - com.example.foobar.Test --> com.example.** 117 // - com.github.someones.repo.Test --> com.github.someones.** 118 String packageName = testClass.getPackage().getName(); 119 String[] packageSegments = packageName.split("\\."); 120 int numSegments = 2; 121 if (packageSegments.length > 2 && packageSegments[0].equals("com") 122 && packageSegments[1].equals("github")) { 123 numSegments = 3; 124 } 125 return Stream.concat(Arrays.stream(packageSegments).limit(numSegments), Stream.of("**")) 126 .collect(joining(".")); 127 } 128 129 private static final Pattern CLASSPATH_SPLITTER = 130 Pattern.compile(Pattern.quote(File.pathSeparator)); 131 132 /** 133 * Returns a heuristic default value for jazzer.instrument based on the files on the provided 134 * classpath. 135 */ getClassPathBasedInstrumentationFilter(String classPath)136 static Optional<String> getClassPathBasedInstrumentationFilter(String classPath) { 137 List<Path> includes = 138 CLASSPATH_SPLITTER.splitAsStream(classPath) 139 .map(Paths::get) 140 // We consider classpath entries that are directories rather than jar files to contain 141 // the classes of the current project rather than external dependencies. This is just a 142 // heuristic and breaks with build systems that package all classes in jar files, e.g. 143 // with Bazel. 144 .filter(Files::isDirectory) 145 .flatMap(root -> { 146 HashSet<Path> pkgs = new HashSet<>(); 147 try { 148 Files.walkFileTree(root, new SimpleFileVisitor<Path>() { 149 @Override 150 public FileVisitResult preVisitDirectory( 151 Path dir, BasicFileAttributes basicFileAttributes) throws IOException { 152 try (Stream<Path> entries = Files.list(dir)) { 153 // If a directory contains a .class file, we add an include filter matching it 154 // and all subdirectories. 155 // Special case: If there is a class defined at the root, only the unnamed 156 // package is included, so continue with the traversal of subdirectories 157 // to discover additional includes. 158 if (entries.filter(path -> path.toString().endsWith(".class")) 159 .anyMatch(Files::isRegularFile)) { 160 Path pkgPath = root.relativize(dir); 161 pkgs.add(pkgPath); 162 if (pkgPath.toString().isEmpty()) { 163 return FileVisitResult.CONTINUE; 164 } else { 165 return FileVisitResult.SKIP_SUBTREE; 166 } 167 } 168 } 169 return FileVisitResult.CONTINUE; 170 } 171 }); 172 } catch (IOException e) { 173 // This is only a best-effort heuristic anyway, ignore this directory. 174 return Stream.of(); 175 } 176 return pkgs.stream(); 177 }) 178 .distinct() 179 .collect(toList()); 180 if (includes.isEmpty()) { 181 return Optional.empty(); 182 } 183 return Optional.of( 184 includes.stream() 185 .map(Path::toString) 186 // For classes without a package, only include the unnamed package. 187 .map(path -> path.isEmpty() ? "*" : path.replace(File.separator, ".") + ".**") 188 .sorted() 189 // jazzer.instrument uses ',' as the separator. 190 .collect(joining(","))); 191 } 192 193 private static final Pattern COVERAGE_AGENT_ARG = 194 Pattern.compile("-javaagent:.*(?:intellij-coverage-agent|jacoco).*"); isCoverageAgentPresent()195 static boolean isCoverageAgentPresent() { 196 return ManagementFactory.getRuntimeMXBean().getInputArguments().stream().anyMatch( 197 s -> COVERAGE_AGENT_ARG.matcher(s).matches()); 198 } 199 200 private static final boolean IS_FUZZING_ENV = 201 System.getenv("JAZZER_FUZZ") != null && !System.getenv("JAZZER_FUZZ").isEmpty(); isFuzzing(ExtensionContext extensionContext)202 static boolean isFuzzing(ExtensionContext extensionContext) { 203 return IS_FUZZING_ENV || runFromCommandLine(extensionContext); 204 } 205 runFromCommandLine(ExtensionContext extensionContext)206 static boolean runFromCommandLine(ExtensionContext extensionContext) { 207 return extensionContext.getConfigurationParameter("jazzer.internal.commandLine") 208 .map(Boolean::parseBoolean) 209 .orElse(false); 210 } 211 212 /** 213 * Returns true if and only if the value is equal to "true", "1", or "yes" case-insensitively. 214 */ permissivelyParseBoolean(String value)215 static boolean permissivelyParseBoolean(String value) { 216 return value.equalsIgnoreCase("true") || value.equals("1") || value.equalsIgnoreCase("yes"); 217 } 218 219 /** 220 * Convert the string to ISO 8601 (https://en.wikipedia.org/wiki/ISO_8601#Durations). We do not 221 * allow for duration units longer than hours, so we can always prepend PT. 222 */ durationStringToSeconds(String duration)223 static long durationStringToSeconds(String duration) { 224 String isoDuration = 225 "PT" + duration.replace("sec", "s").replace("min", "m").replace("hr", "h").replace(" ", ""); 226 return Duration.parse(isoDuration).getSeconds(); 227 } 228 229 /** 230 * Creates {@link Arguments} for a single invocation of a parameterized test that can be 231 * identified as having been created in this way by {@link #isMarkedInvocation}. 232 * 233 * @param displayName the display name to assign to every argument 234 */ getMarkedArguments(Method method, String displayName)235 static Arguments getMarkedArguments(Method method, String displayName) { 236 return arguments(stream(method.getParameterTypes()) 237 .map(Utils::getMarkedInstance) 238 // Wrap in named as toString may crash on marked instances. 239 .map(arg -> named(displayName, arg)) 240 .toArray(Object[] ::new)); 241 } 242 243 /** 244 * @return {@code true} if and only if the arguments for this test method invocation were created 245 * with {@link #getMarkedArguments} 246 */ isMarkedInvocation(ReflectiveInvocationContext<Method> invocationContext)247 static boolean isMarkedInvocation(ReflectiveInvocationContext<Method> invocationContext) { 248 if (invocationContext.getArguments().stream().anyMatch(Utils::isMarkedInstance)) { 249 if (invocationContext.getArguments().stream().allMatch(Utils::isMarkedInstance)) { 250 return true; 251 } 252 throw new IllegalStateException( 253 "Some, but not all arguments were marked in invocation of " + invocationContext); 254 } else { 255 return false; 256 } 257 } 258 259 private static final ClassValue<Object> uniqueInstanceCache = new ClassValue<Object>() { 260 @Override 261 protected Object computeValue(Class<?> clazz) { 262 return makeMarkedInstance(clazz); 263 } 264 }; 265 private static final Set<Object> uniqueInstances = newSetFromMap(new IdentityHashMap<>()); 266 267 // Visible for testing. getMarkedInstance(Class<T> clazz)268 static <T> T getMarkedInstance(Class<T> clazz) { 269 // makeMarkedInstance creates new classes, which is expensive and can cause the JVM to run out 270 // of metaspace. We thus cache the marked instances per class. 271 Object instance = uniqueInstanceCache.get(clazz); 272 uniqueInstances.add(instance); 273 return (T) instance; 274 } 275 276 // Visible for testing. isMarkedInstance(Object instance)277 static boolean isMarkedInstance(Object instance) { 278 return uniqueInstances.contains(instance); 279 } 280 makeMarkedInstance(Class<?> clazz)281 private static Object makeMarkedInstance(Class<?> clazz) { 282 if (clazz == Class.class) { 283 return new Object() {}.getClass(); 284 } 285 if (clazz.isArray()) { 286 return Array.newInstance(clazz.getComponentType(), 0); 287 } 288 if (clazz.isInterface()) { 289 return Proxy.newProxyInstance( 290 Utils.class.getClassLoader(), new Class[] {clazz}, (o, method, objects) -> null); 291 } 292 293 if (clazz.isPrimitive()) { 294 clazz = MethodType.methodType(clazz).wrap().returnType(); 295 } else if (Modifier.isAbstract(clazz.getModifiers())) { 296 clazz = UnsafeUtils.defineAnonymousConcreteSubclass(clazz); 297 } 298 299 try { 300 return clazz.cast(UnsafeProvider.getUnsafe().allocateInstance(clazz)); 301 } catch (InstantiationException e) { 302 throw new IllegalStateException(e); 303 } 304 } 305 } 306