// 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[]> throwsDeclarations; private static Set 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 ignoredExceptionGlobMatchers = Arrays.stream(args) .skip(1) .filter(s -> s.contains("*")) .map(SimpleGlobMatcher::new) .collect(Collectors.toSet()); List> 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[]> 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 ignoredExceptionMatchers, Map[]> 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(); } } }