aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java')
-rw-r--r--src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java363
1 files changed, 363 insertions, 0 deletions
diff --git a/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java b/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java
new file mode 100644
index 00000000..885ebfbf
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java
@@ -0,0 +1,363 @@
+// Copyright 2023 Code Intelligence GmbH
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.code_intelligence.jazzer.autofuzz;
+
+import com.code_intelligence.jazzer.api.AutofuzzConstructionException;
+import com.code_intelligence.jazzer.api.AutofuzzInvocationException;
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.utils.Log;
+import com.code_intelligence.jazzer.utils.SimpleGlobMatcher;
+import com.code_intelligence.jazzer.utils.Utils;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public final class FuzzTarget {
+ private static final String AUTOFUZZ_REPRODUCER_TEMPLATE = "public class Crash_%1$s {\n"
+ + " public static void main(String[] args) throws Throwable {\n"
+ + " Crash_%1$s.class.getClassLoader().setDefaultAssertionStatus(true);\n"
+ + " %2$s;\n"
+ + " }\n"
+ + "}";
+ private static final long MAX_EXECUTIONS_WITHOUT_INVOCATION = 100;
+
+ private static Meta meta;
+ private static String methodReference;
+ private static Executable[] targetExecutables;
+ private static Object targetInstance;
+ private static Map<Executable, Class<?>[]> throwsDeclarations;
+ private static Set<SimpleGlobMatcher> ignoredExceptionMatchers;
+ private static long executionsSinceLastInvocation = 0;
+
+ public static void fuzzerInitialize(String[] args) {
+ if (args.length == 0 || !args[0].contains("::")) {
+ Log.error(
+ "Expected the argument to --autofuzz to be a method reference (e.g. System.out::println)");
+ System.exit(1);
+ }
+ String methodSignature = args[0];
+ String[] parts = methodSignature.split("::", 2);
+ String className = parts[0];
+ String methodNameAndOptionalDescriptor = parts[1];
+ String methodName;
+ String descriptor;
+ int descriptorStart = methodNameAndOptionalDescriptor.indexOf('(');
+ if (descriptorStart != -1) {
+ methodName = methodNameAndOptionalDescriptor.substring(0, descriptorStart);
+ // URL decode the descriptor to allow copy-pasting from javadoc links such as:
+ // https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#valueOf(char%5B%5D)
+ try {
+ descriptor =
+ URLDecoder.decode(methodNameAndOptionalDescriptor.substring(descriptorStart), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // UTF-8 is always supported.
+ Log.error(e);
+ System.exit(1);
+ return;
+ }
+ } else {
+ methodName = methodNameAndOptionalDescriptor;
+ descriptor = null;
+ }
+
+ Class<?> targetClassTemp = null;
+ String targetClassName = className;
+ do {
+ try {
+ targetClassTemp = Class.forName(targetClassName);
+ } catch (ClassNotFoundException e) {
+ int classSeparatorIndex = targetClassName.lastIndexOf(".");
+ if (classSeparatorIndex == -1) {
+ Log.error(String.format(
+ "Failed to find class %s for autofuzz, please ensure it is contained in the classpath specified with --cp and specify the full package name",
+ className));
+ System.exit(1);
+ return;
+ }
+ StringBuilder classNameBuilder = new StringBuilder(targetClassName);
+ classNameBuilder.setCharAt(classSeparatorIndex, '$');
+ targetClassName = classNameBuilder.toString();
+ }
+ } while (targetClassTemp == null);
+ final Class<?> targetClass = targetClassTemp;
+
+ AccessibleObjectLookup lookup = new AccessibleObjectLookup(targetClass);
+
+ Executable[] executables;
+ boolean isConstructor = methodName.equals("new");
+ // We filter out inherited methods, which can lead to unexpected results when autofuzzing a
+ // method by name without a descriptor. If desired, these can be autofuzzed explicitly by
+ // referencing the parent class. If a descriptor is provided, we also allow fuzzing non-public
+ // methods. This is necessary e.g. when using Autofuzz on a package-private JUnit @FuzzTest
+ // method.
+ if (isConstructor) {
+ executables = Arrays.stream(lookup.getAccessibleConstructors(targetClass))
+ .filter(constructor -> constructor.getDeclaringClass().equals(targetClass))
+ .filter(constructor
+ -> (descriptor == null && Modifier.isPublic(constructor.getModifiers()))
+ || Utils.getReadableDescriptor(constructor).equals(descriptor))
+ .toArray(Executable[] ::new);
+ } else {
+ executables = Arrays.stream(lookup.getAccessibleMethods(targetClass))
+ .filter(method -> method.getDeclaringClass().equals(targetClass))
+ .filter(method
+ -> method.getName().equals(methodName)
+ && ((descriptor == null && Modifier.isPublic(method.getModifiers()))
+ || Utils.getReadableDescriptor(method).equals(descriptor)))
+ .toArray(Executable[] ::new);
+ }
+ if (executables.length == 0) {
+ if (isConstructor) {
+ if (descriptor == null) {
+ Log.error(
+ String.format("Failed to find constructors in class %s for autofuzz.%n", className));
+ } else {
+ Log.error(String.format(
+ "Failed to find constructors with signature %s in class %s for autofuzz.%n"
+ + "Public constructors declared by the class:%n%s",
+ descriptor, className,
+ Arrays.stream(lookup.getAccessibleConstructors(targetClass))
+ .filter(constructor -> Modifier.isPublic(constructor.getModifiers()))
+ .filter(constructor -> constructor.getDeclaringClass().equals(targetClass))
+ .map(method
+ -> String.format("%s::new%s", method.getDeclaringClass().getName(),
+ Utils.getReadableDescriptor(method)))
+ .distinct()
+ .collect(Collectors.joining(System.lineSeparator()))));
+ }
+ } else {
+ if (descriptor == null) {
+ Log.error(String.format("Failed to find methods named %s in class %s for autofuzz.%n"
+ + "Public methods declared by the class:%n%s",
+ methodName, className,
+ Arrays.stream(lookup.getAccessibleMethods(targetClass))
+ .filter(method -> Modifier.isPublic(method.getModifiers()))
+ .filter(method -> method.getDeclaringClass().equals(targetClass))
+ .map(method
+ -> String.format(
+ "%s::%s", method.getDeclaringClass().getName(), method.getName()))
+ .distinct()
+ .collect(Collectors.joining(System.lineSeparator()))));
+ } else {
+ Log.error(String.format(
+ "Failed to find public methods named %s with signature %s in class %s for autofuzz.%n"
+ + "Public methods with that name:%n%s",
+ methodName, descriptor, className,
+ Arrays.stream(lookup.getAccessibleMethods(targetClass))
+ .filter(method -> Modifier.isPublic(method.getModifiers()))
+ .filter(method -> method.getDeclaringClass().equals(targetClass))
+ .filter(method -> method.getName().equals(methodName))
+ .map(method
+ -> String.format("%s::%s%s", method.getDeclaringClass().getName(),
+ method.getName(), Utils.getReadableDescriptor(method)))
+ .distinct()
+ .collect(Collectors.joining(System.lineSeparator()))));
+ }
+ }
+ System.exit(1);
+ }
+
+ Set<SimpleGlobMatcher> ignoredExceptionGlobMatchers = Arrays.stream(args)
+ .skip(1)
+ .filter(s -> s.contains("*"))
+ .map(SimpleGlobMatcher::new)
+ .collect(Collectors.toSet());
+
+ List<Class<?>> alwaysIgnore =
+ Arrays.stream(args)
+ .skip(1)
+ .filter(s -> !s.contains("*"))
+ .map(name -> {
+ try {
+ return ClassLoader.getSystemClassLoader().loadClass(name);
+ } catch (ClassNotFoundException e) {
+ Log.error(String.format(
+ "Failed to find class '%s' specified in --autofuzz_ignore", name));
+ System.exit(1);
+ }
+ throw new Error("Not reached");
+ })
+ .collect(Collectors.toList());
+
+ Map<Executable, Class<?>[]> ignoredExceptionClasses =
+ Arrays.stream(executables)
+ .collect(Collectors.toMap(method
+ -> method,
+ method
+ -> Stream.concat(Arrays.stream(method.getExceptionTypes()), alwaysIgnore.stream())
+ .toArray(Class[] ::new)));
+
+ setTarget(
+ executables, null, methodSignature, ignoredExceptionGlobMatchers, ignoredExceptionClasses);
+ }
+
+ /**
+ * Set the target executables to (auto-)fuzz. This method is primarily used by the JUnit
+ * integration to set the target class and method passed in by the test framework.
+ */
+ public static void setTarget(Executable[] targetExecutables, Object targetInstance,
+ String methodReference, Set<SimpleGlobMatcher> ignoredExceptionMatchers,
+ Map<Executable, Class<?>[]> throwsDeclarations) {
+ Class<?> targetClass = null;
+ for (Executable executable : targetExecutables) {
+ if (targetClass != null && !targetClass.equals(executable.getDeclaringClass())) {
+ throw new IllegalStateException(
+ "All target executables must be declared in the same class");
+ }
+ targetClass = executable.getDeclaringClass();
+ executable.setAccessible(true);
+ }
+
+ FuzzTarget.meta = new Meta(targetClass);
+ FuzzTarget.targetExecutables = targetExecutables;
+ FuzzTarget.targetInstance = targetInstance;
+ FuzzTarget.methodReference = methodReference;
+ FuzzTarget.ignoredExceptionMatchers = ignoredExceptionMatchers;
+ FuzzTarget.throwsDeclarations = throwsDeclarations;
+ }
+
+ public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Throwable {
+ AutofuzzCodegenVisitor codegenVisitor = null;
+ if (Meta.IS_DEBUG) {
+ codegenVisitor = new AutofuzzCodegenVisitor();
+ }
+ fuzzerTestOneInput(data, codegenVisitor);
+ if (codegenVisitor != null) {
+ Log.println(codegenVisitor.generate());
+ }
+ }
+
+ public static void dumpReproducer(FuzzedDataProvider data, String reproducerPath, String sha) {
+ AutofuzzCodegenVisitor codegenVisitor = new AutofuzzCodegenVisitor();
+ try {
+ fuzzerTestOneInput(data, codegenVisitor);
+ } catch (Throwable ignored) {
+ }
+ String javaSource = String.format(AUTOFUZZ_REPRODUCER_TEMPLATE, sha, codegenVisitor.generate());
+ Path javaPath = Paths.get(reproducerPath, String.format("Crash_%s.java", sha));
+ try {
+ Files.write(javaPath, javaSource.getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ Log.error(String.format("Failed to write Java reproducer to %s%n", javaPath), e);
+ }
+ Log.println(String.format(
+ "reproducer_path='%s'; Java reproducer written to %s%n", reproducerPath, javaPath));
+ }
+
+ private static void fuzzerTestOneInput(
+ FuzzedDataProvider data, AutofuzzCodegenVisitor codegenVisitor) throws Throwable {
+ Executable targetExecutable;
+ if (FuzzTarget.targetExecutables.length == 1) {
+ targetExecutable = FuzzTarget.targetExecutables[0];
+ } else {
+ targetExecutable = data.pickValue(FuzzTarget.targetExecutables);
+ }
+ Object returnValue = null;
+ try {
+ if (targetExecutable instanceof Method) {
+ if (targetInstance != null) {
+ returnValue =
+ meta.autofuzz(data, (Method) targetExecutable, targetInstance, codegenVisitor);
+ } else {
+ returnValue = meta.autofuzz(data, (Method) targetExecutable, codegenVisitor);
+ }
+ } else {
+ // No targetInstance for constructors possible.
+ returnValue = meta.autofuzz(data, (Constructor<?>) targetExecutable, codegenVisitor);
+ }
+ executionsSinceLastInvocation = 0;
+ } catch (AutofuzzConstructionException e) {
+ if (Meta.IS_DEBUG) {
+ Log.error(e);
+ }
+ // Ignore exceptions thrown while constructing the parameters for the target method. We can
+ // only guess how to generate valid parameters and any exceptions thrown while doing so
+ // are most likely on us. However, if this happens too often, Autofuzz got stuck and we should
+ // let the user know.
+ executionsSinceLastInvocation++;
+ if (executionsSinceLastInvocation >= MAX_EXECUTIONS_WITHOUT_INVOCATION) {
+ Log.error(
+ String.format("Failed to generate valid arguments to '%s' in %d attempts; giving up",
+ methodReference, executionsSinceLastInvocation));
+ System.exit(1);
+ } else if (executionsSinceLastInvocation == MAX_EXECUTIONS_WITHOUT_INVOCATION / 2) {
+ // The application under test might perform classpath modifications or create classes
+ // dynamically that implement interfaces or extend abstract classes. Rescanning the
+ // classpath might help with constructing objects.
+ Meta.rescanClasspath();
+ }
+ } catch (AutofuzzInvocationException e) {
+ executionsSinceLastInvocation = 0;
+ Throwable cause = e.getCause();
+ Class<?> causeClass = cause.getClass();
+ // Do not report exceptions declared to be thrown by the method under test.
+ for (Class<?> declaredThrow :
+ throwsDeclarations.getOrDefault(targetExecutable, new Class[0])) {
+ if (declaredThrow.isAssignableFrom(causeClass)) {
+ return;
+ }
+ }
+
+ if (ignoredExceptionMatchers.stream().anyMatch(m -> m.matches(causeClass.getName()))) {
+ return;
+ }
+ cleanStackTraces(cause);
+ throw cause;
+ } catch (Throwable t) {
+ Log.error("Unexpected exception encountered during autofuzz", t);
+ System.exit(1);
+ } finally {
+ if (returnValue instanceof Closeable) {
+ ((Closeable) returnValue).close();
+ }
+ }
+ }
+
+ // Removes all stack trace elements that live in the Java reflection packages or the autofuzz
+ // package from the bottom of all stack frames.
+ private static void cleanStackTraces(Throwable t) {
+ Throwable cause = t;
+ while (cause != null) {
+ StackTraceElement[] elements = cause.getStackTrace();
+ int firstInterestingPos;
+ for (firstInterestingPos = elements.length - 1; firstInterestingPos > 0;
+ firstInterestingPos--) {
+ String className = elements[firstInterestingPos].getClassName();
+ if (!className.startsWith("com.code_intelligence.jazzer.autofuzz.")
+ && !className.startsWith("java.lang.reflect.")
+ && !className.startsWith("jdk.internal.reflect.")) {
+ break;
+ }
+ }
+ cause.setStackTrace(Arrays.copyOfRange(elements, 0, firstInterestingPos + 1));
+ cause = cause.getCause();
+ }
+ }
+}