1 // Copyright 2023 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.autofuzz; 16 17 import com.code_intelligence.jazzer.api.AutofuzzConstructionException; 18 import com.code_intelligence.jazzer.api.AutofuzzInvocationException; 19 import com.code_intelligence.jazzer.api.FuzzedDataProvider; 20 import com.code_intelligence.jazzer.utils.Log; 21 import com.code_intelligence.jazzer.utils.SimpleGlobMatcher; 22 import com.code_intelligence.jazzer.utils.Utils; 23 import java.io.Closeable; 24 import java.io.IOException; 25 import java.io.UnsupportedEncodingException; 26 import java.lang.reflect.Constructor; 27 import java.lang.reflect.Executable; 28 import java.lang.reflect.Method; 29 import java.lang.reflect.Modifier; 30 import java.net.URLDecoder; 31 import java.nio.charset.StandardCharsets; 32 import java.nio.file.Files; 33 import java.nio.file.Path; 34 import java.nio.file.Paths; 35 import java.util.Arrays; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.Set; 39 import java.util.stream.Collectors; 40 import java.util.stream.Stream; 41 42 public final class FuzzTarget { 43 private static final String AUTOFUZZ_REPRODUCER_TEMPLATE = "public class Crash_%1$s {\n" 44 + " public static void main(String[] args) throws Throwable {\n" 45 + " Crash_%1$s.class.getClassLoader().setDefaultAssertionStatus(true);\n" 46 + " %2$s;\n" 47 + " }\n" 48 + "}"; 49 private static final long MAX_EXECUTIONS_WITHOUT_INVOCATION = 100; 50 51 private static Meta meta; 52 private static String methodReference; 53 private static Executable[] targetExecutables; 54 private static Object targetInstance; 55 private static Map<Executable, Class<?>[]> throwsDeclarations; 56 private static Set<SimpleGlobMatcher> ignoredExceptionMatchers; 57 private static long executionsSinceLastInvocation = 0; 58 fuzzerInitialize(String[] args)59 public static void fuzzerInitialize(String[] args) { 60 if (args.length == 0 || !args[0].contains("::")) { 61 Log.error( 62 "Expected the argument to --autofuzz to be a method reference (e.g. System.out::println)"); 63 System.exit(1); 64 } 65 String methodSignature = args[0]; 66 String[] parts = methodSignature.split("::", 2); 67 String className = parts[0]; 68 String methodNameAndOptionalDescriptor = parts[1]; 69 String methodName; 70 String descriptor; 71 int descriptorStart = methodNameAndOptionalDescriptor.indexOf('('); 72 if (descriptorStart != -1) { 73 methodName = methodNameAndOptionalDescriptor.substring(0, descriptorStart); 74 // URL decode the descriptor to allow copy-pasting from javadoc links such as: 75 // https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#valueOf(char%5B%5D) 76 try { 77 descriptor = 78 URLDecoder.decode(methodNameAndOptionalDescriptor.substring(descriptorStart), "UTF-8"); 79 } catch (UnsupportedEncodingException e) { 80 // UTF-8 is always supported. 81 Log.error(e); 82 System.exit(1); 83 return; 84 } 85 } else { 86 methodName = methodNameAndOptionalDescriptor; 87 descriptor = null; 88 } 89 90 Class<?> targetClassTemp = null; 91 String targetClassName = className; 92 do { 93 try { 94 targetClassTemp = Class.forName(targetClassName); 95 } catch (ClassNotFoundException e) { 96 int classSeparatorIndex = targetClassName.lastIndexOf("."); 97 if (classSeparatorIndex == -1) { 98 Log.error(String.format( 99 "Failed to find class %s for autofuzz, please ensure it is contained in the classpath specified with --cp and specify the full package name", 100 className)); 101 System.exit(1); 102 return; 103 } 104 StringBuilder classNameBuilder = new StringBuilder(targetClassName); 105 classNameBuilder.setCharAt(classSeparatorIndex, '$'); 106 targetClassName = classNameBuilder.toString(); 107 } 108 } while (targetClassTemp == null); 109 final Class<?> targetClass = targetClassTemp; 110 111 AccessibleObjectLookup lookup = new AccessibleObjectLookup(targetClass); 112 113 Executable[] executables; 114 boolean isConstructor = methodName.equals("new"); 115 // We filter out inherited methods, which can lead to unexpected results when autofuzzing a 116 // method by name without a descriptor. If desired, these can be autofuzzed explicitly by 117 // referencing the parent class. If a descriptor is provided, we also allow fuzzing non-public 118 // methods. This is necessary e.g. when using Autofuzz on a package-private JUnit @FuzzTest 119 // method. 120 if (isConstructor) { 121 executables = Arrays.stream(lookup.getAccessibleConstructors(targetClass)) 122 .filter(constructor -> constructor.getDeclaringClass().equals(targetClass)) 123 .filter(constructor 124 -> (descriptor == null && Modifier.isPublic(constructor.getModifiers())) 125 || Utils.getReadableDescriptor(constructor).equals(descriptor)) 126 .toArray(Executable[] ::new); 127 } else { 128 executables = Arrays.stream(lookup.getAccessibleMethods(targetClass)) 129 .filter(method -> method.getDeclaringClass().equals(targetClass)) 130 .filter(method 131 -> method.getName().equals(methodName) 132 && ((descriptor == null && Modifier.isPublic(method.getModifiers())) 133 || Utils.getReadableDescriptor(method).equals(descriptor))) 134 .toArray(Executable[] ::new); 135 } 136 if (executables.length == 0) { 137 if (isConstructor) { 138 if (descriptor == null) { 139 Log.error( 140 String.format("Failed to find constructors in class %s for autofuzz.%n", className)); 141 } else { 142 Log.error(String.format( 143 "Failed to find constructors with signature %s in class %s for autofuzz.%n" 144 + "Public constructors declared by the class:%n%s", 145 descriptor, className, 146 Arrays.stream(lookup.getAccessibleConstructors(targetClass)) 147 .filter(constructor -> Modifier.isPublic(constructor.getModifiers())) 148 .filter(constructor -> constructor.getDeclaringClass().equals(targetClass)) 149 .map(method 150 -> String.format("%s::new%s", method.getDeclaringClass().getName(), 151 Utils.getReadableDescriptor(method))) 152 .distinct() 153 .collect(Collectors.joining(System.lineSeparator())))); 154 } 155 } else { 156 if (descriptor == null) { 157 Log.error(String.format("Failed to find methods named %s in class %s for autofuzz.%n" 158 + "Public methods declared by the class:%n%s", 159 methodName, className, 160 Arrays.stream(lookup.getAccessibleMethods(targetClass)) 161 .filter(method -> Modifier.isPublic(method.getModifiers())) 162 .filter(method -> method.getDeclaringClass().equals(targetClass)) 163 .map(method 164 -> String.format( 165 "%s::%s", method.getDeclaringClass().getName(), method.getName())) 166 .distinct() 167 .collect(Collectors.joining(System.lineSeparator())))); 168 } else { 169 Log.error(String.format( 170 "Failed to find public methods named %s with signature %s in class %s for autofuzz.%n" 171 + "Public methods with that name:%n%s", 172 methodName, descriptor, className, 173 Arrays.stream(lookup.getAccessibleMethods(targetClass)) 174 .filter(method -> Modifier.isPublic(method.getModifiers())) 175 .filter(method -> method.getDeclaringClass().equals(targetClass)) 176 .filter(method -> method.getName().equals(methodName)) 177 .map(method 178 -> String.format("%s::%s%s", method.getDeclaringClass().getName(), 179 method.getName(), Utils.getReadableDescriptor(method))) 180 .distinct() 181 .collect(Collectors.joining(System.lineSeparator())))); 182 } 183 } 184 System.exit(1); 185 } 186 187 Set<SimpleGlobMatcher> ignoredExceptionGlobMatchers = Arrays.stream(args) 188 .skip(1) 189 .filter(s -> s.contains("*")) 190 .map(SimpleGlobMatcher::new) 191 .collect(Collectors.toSet()); 192 193 List<Class<?>> alwaysIgnore = 194 Arrays.stream(args) 195 .skip(1) 196 .filter(s -> !s.contains("*")) 197 .map(name -> { 198 try { 199 return ClassLoader.getSystemClassLoader().loadClass(name); 200 } catch (ClassNotFoundException e) { 201 Log.error(String.format( 202 "Failed to find class '%s' specified in --autofuzz_ignore", name)); 203 System.exit(1); 204 } 205 throw new Error("Not reached"); 206 }) 207 .collect(Collectors.toList()); 208 209 Map<Executable, Class<?>[]> ignoredExceptionClasses = 210 Arrays.stream(executables) 211 .collect(Collectors.toMap(method 212 -> method, 213 method 214 -> Stream.concat(Arrays.stream(method.getExceptionTypes()), alwaysIgnore.stream()) 215 .toArray(Class[] ::new))); 216 217 setTarget( 218 executables, null, methodSignature, ignoredExceptionGlobMatchers, ignoredExceptionClasses); 219 } 220 221 /** 222 * Set the target executables to (auto-)fuzz. This method is primarily used by the JUnit 223 * integration to set the target class and method passed in by the test framework. 224 */ setTarget(Executable[] targetExecutables, Object targetInstance, String methodReference, Set<SimpleGlobMatcher> ignoredExceptionMatchers, Map<Executable, Class<?>[]> throwsDeclarations)225 public static void setTarget(Executable[] targetExecutables, Object targetInstance, 226 String methodReference, Set<SimpleGlobMatcher> ignoredExceptionMatchers, 227 Map<Executable, Class<?>[]> throwsDeclarations) { 228 Class<?> targetClass = null; 229 for (Executable executable : targetExecutables) { 230 if (targetClass != null && !targetClass.equals(executable.getDeclaringClass())) { 231 throw new IllegalStateException( 232 "All target executables must be declared in the same class"); 233 } 234 targetClass = executable.getDeclaringClass(); 235 executable.setAccessible(true); 236 } 237 238 FuzzTarget.meta = new Meta(targetClass); 239 FuzzTarget.targetExecutables = targetExecutables; 240 FuzzTarget.targetInstance = targetInstance; 241 FuzzTarget.methodReference = methodReference; 242 FuzzTarget.ignoredExceptionMatchers = ignoredExceptionMatchers; 243 FuzzTarget.throwsDeclarations = throwsDeclarations; 244 } 245 fuzzerTestOneInput(FuzzedDataProvider data)246 public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Throwable { 247 AutofuzzCodegenVisitor codegenVisitor = null; 248 if (Meta.IS_DEBUG) { 249 codegenVisitor = new AutofuzzCodegenVisitor(); 250 } 251 fuzzerTestOneInput(data, codegenVisitor); 252 if (codegenVisitor != null) { 253 Log.println(codegenVisitor.generate()); 254 } 255 } 256 dumpReproducer(FuzzedDataProvider data, String reproducerPath, String sha)257 public static void dumpReproducer(FuzzedDataProvider data, String reproducerPath, String sha) { 258 AutofuzzCodegenVisitor codegenVisitor = new AutofuzzCodegenVisitor(); 259 try { 260 fuzzerTestOneInput(data, codegenVisitor); 261 } catch (Throwable ignored) { 262 } 263 String javaSource = String.format(AUTOFUZZ_REPRODUCER_TEMPLATE, sha, codegenVisitor.generate()); 264 Path javaPath = Paths.get(reproducerPath, String.format("Crash_%s.java", sha)); 265 try { 266 Files.write(javaPath, javaSource.getBytes(StandardCharsets.UTF_8)); 267 } catch (IOException e) { 268 Log.error(String.format("Failed to write Java reproducer to %s%n", javaPath), e); 269 } 270 Log.println(String.format( 271 "reproducer_path='%s'; Java reproducer written to %s%n", reproducerPath, javaPath)); 272 } 273 fuzzerTestOneInput( FuzzedDataProvider data, AutofuzzCodegenVisitor codegenVisitor)274 private static void fuzzerTestOneInput( 275 FuzzedDataProvider data, AutofuzzCodegenVisitor codegenVisitor) throws Throwable { 276 Executable targetExecutable; 277 if (FuzzTarget.targetExecutables.length == 1) { 278 targetExecutable = FuzzTarget.targetExecutables[0]; 279 } else { 280 targetExecutable = data.pickValue(FuzzTarget.targetExecutables); 281 } 282 Object returnValue = null; 283 try { 284 if (targetExecutable instanceof Method) { 285 if (targetInstance != null) { 286 returnValue = 287 meta.autofuzz(data, (Method) targetExecutable, targetInstance, codegenVisitor); 288 } else { 289 returnValue = meta.autofuzz(data, (Method) targetExecutable, codegenVisitor); 290 } 291 } else { 292 // No targetInstance for constructors possible. 293 returnValue = meta.autofuzz(data, (Constructor<?>) targetExecutable, codegenVisitor); 294 } 295 executionsSinceLastInvocation = 0; 296 } catch (AutofuzzConstructionException e) { 297 if (Meta.IS_DEBUG) { 298 Log.error(e); 299 } 300 // Ignore exceptions thrown while constructing the parameters for the target method. We can 301 // only guess how to generate valid parameters and any exceptions thrown while doing so 302 // are most likely on us. However, if this happens too often, Autofuzz got stuck and we should 303 // let the user know. 304 executionsSinceLastInvocation++; 305 if (executionsSinceLastInvocation >= MAX_EXECUTIONS_WITHOUT_INVOCATION) { 306 Log.error( 307 String.format("Failed to generate valid arguments to '%s' in %d attempts; giving up", 308 methodReference, executionsSinceLastInvocation)); 309 System.exit(1); 310 } else if (executionsSinceLastInvocation == MAX_EXECUTIONS_WITHOUT_INVOCATION / 2) { 311 // The application under test might perform classpath modifications or create classes 312 // dynamically that implement interfaces or extend abstract classes. Rescanning the 313 // classpath might help with constructing objects. 314 Meta.rescanClasspath(); 315 } 316 } catch (AutofuzzInvocationException e) { 317 executionsSinceLastInvocation = 0; 318 Throwable cause = e.getCause(); 319 Class<?> causeClass = cause.getClass(); 320 // Do not report exceptions declared to be thrown by the method under test. 321 for (Class<?> declaredThrow : 322 throwsDeclarations.getOrDefault(targetExecutable, new Class[0])) { 323 if (declaredThrow.isAssignableFrom(causeClass)) { 324 return; 325 } 326 } 327 328 if (ignoredExceptionMatchers.stream().anyMatch(m -> m.matches(causeClass.getName()))) { 329 return; 330 } 331 cleanStackTraces(cause); 332 throw cause; 333 } catch (Throwable t) { 334 Log.error("Unexpected exception encountered during autofuzz", t); 335 System.exit(1); 336 } finally { 337 if (returnValue instanceof Closeable) { 338 ((Closeable) returnValue).close(); 339 } 340 } 341 } 342 343 // Removes all stack trace elements that live in the Java reflection packages or the autofuzz 344 // package from the bottom of all stack frames. cleanStackTraces(Throwable t)345 private static void cleanStackTraces(Throwable t) { 346 Throwable cause = t; 347 while (cause != null) { 348 StackTraceElement[] elements = cause.getStackTrace(); 349 int firstInterestingPos; 350 for (firstInterestingPos = elements.length - 1; firstInterestingPos > 0; 351 firstInterestingPos--) { 352 String className = elements[firstInterestingPos].getClassName(); 353 if (!className.startsWith("com.code_intelligence.jazzer.autofuzz.") 354 && !className.startsWith("java.lang.reflect.") 355 && !className.startsWith("jdk.internal.reflect.")) { 356 break; 357 } 358 } 359 cause.setStackTrace(Arrays.copyOfRange(elements, 0, firstInterestingPos + 1)); 360 cause = cause.getCause(); 361 } 362 } 363 } 364