diff options
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.java | 363 |
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(); + } + } +} |