• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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