diff options
Diffstat (limited to 'src/main/java/com')
179 files changed, 21542 insertions, 0 deletions
diff --git a/src/main/java/com/code_intelligence/jazzer/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/BUILD.bazel new file mode 100644 index 00000000..aed6769d --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/BUILD.bazel @@ -0,0 +1,137 @@ +load("@bazel_skylib//rules:write_file.bzl", "write_file") +load("@com_github_johnynek_bazel_jar_jar//:jar_jar.bzl", "jar_jar") +load("@rules_jvm_external//:defs.bzl", "javadoc") +load("//:maven.bzl", "JAZZER_VERSION") +load("//bazel:jar.bzl", "strip_jar") +load("//sanitizers:sanitizers.bzl", "SANITIZER_CLASSES") + +java_binary( + name = "jazzer_standalone", + main_class = "com.code_intelligence.jazzer.Jazzer", + visibility = [ + "//:__pkg__", + "//launcher:__pkg__", + ], + runtime_deps = [ + ":jazzer_import", + "//deploy:jazzer-api", + ], +) + +strip_jar( + name = "jazzer", + out = "jazzer.jar", + jar = ":jazzer_shaded", + paths_to_keep = [ + "com/code_intelligence/jazzer/**", + "jaz/**", + "META-INF/MANIFEST.MF", + "win32-x86/**", + "win32-x86-64/**", + ], + visibility = ["//visibility:public"], +) + +java_library( + name = "constants", + srcs = [":constants_java"], + visibility = ["//visibility:public"], +) + +java_import( + name = "jazzer_import", + jars = [":jazzer"], + visibility = ["//:__subpackages__"], + deps = ["//deploy:jazzer-api"], +) + +jar_jar( + name = "jazzer_shaded", + input_jar = "jazzer_unshaded_deploy.jar", + rules = "jazzer_shade_rules.jarjar", +) + +java_binary( + name = "jazzer_unshaded", + # Note: We can't add + # //src/main/java/com/code_intelligence/jazzer/runtime:java_bootstrap_unshaded itself as + # that would also strip out external dependencies common between Jazzer and its bootstrap jar, + # such as e.g. RulesJni, which should be shaded into distinct classes. + deploy_env = [ + "//src/main/java/com/code_intelligence/jazzer/api:api_deploy_env", + "//src/main/java/com/code_intelligence/jazzer/runtime:jazzer_bootstrap_env", + ], + main_class = "com.code_intelligence.jazzer.Jazzer", + runtime_deps = [":jazzer_lib"], +) + +# Docs are only generated for the com.code_intelligence.jazzer package. Everything else is not +# considered a public interface. +javadoc( + name = "jazzer-docs", + javadocopts = select({ + "//deploy:emit_linked_javadoc": [ + "-link", + "https://docs.oracle.com/en/java/javase/17/docs/api/", + "-link", + "https://codeintelligencetesting.github.io/jazzer-docs/jazzer-api/", + ], + "//conditions:default": [], + }), + visibility = ["//deploy:__pkg__"], + deps = [":jazzer_lib"], +) + +strip_jar( + name = "jazzer-sources", + jar = ":jazzer_transitive_sources_deploy-src.jar", + paths_to_keep = [ + "com/code_intelligence/jazzer/**", + "jaz/**", + "META-INF/MANIFEST.MF", + ], + visibility = ["//deploy:__pkg__"], +) + +# The _deploy-src.jar for this target includes the sources for the jazzer_bootstrap library. +java_binary( + name = "jazzer_transitive_sources", + main_class = "com.code_intelligence.jazzer.Jazzer", + runtime_deps = [ + ":jazzer_lib", + "//src/main/java/com/code_intelligence/jazzer/runtime:jazzer_bootstrap_lib", + ], +) + +java_library( + name = "jazzer_lib", + srcs = ["Jazzer.java"], + visibility = ["//deploy:__pkg__"], + runtime_deps = select({ + "@platforms//os:windows": [], + "//conditions:default": ["//src/main/native/com/code_intelligence/jazzer:jazzer_preload"], + }) + [ + # Only used by JUnit, but including it here means we don't need to shade ASM in + # jazzer-junit. + "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_utils", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/android:android_runtime", + "//src/main/java/com/code_intelligence/jazzer/driver", + "//src/main/java/com/code_intelligence/jazzer/runtime:constants", + "//src/main/java/com/code_intelligence/jazzer/utils:log", + "//src/main/java/com/code_intelligence/jazzer/utils:zip_utils", + "@fmeum_rules_jni//jni/tools/native_loader", + ], +) + +write_file( + name = "constants_java", + out = "Constants.java", + content = [ + "package com.code_intelligence.jazzer;", + "public final class Constants {", + " public static final String JAZZER_VERSION = \"%s\";" % JAZZER_VERSION, + "}", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/Jazzer.java b/src/main/java/com/code_intelligence/jazzer/Jazzer.java new file mode 100644 index 00000000..3eb316dd --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/Jazzer.java @@ -0,0 +1,515 @@ +/* + * Copyright 2022 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; + +import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID; +import static java.lang.System.exit; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + +import com.code_intelligence.jazzer.android.AndroidRuntime; +import com.code_intelligence.jazzer.driver.Driver; +import com.code_intelligence.jazzer.utils.Log; +import com.code_intelligence.jazzer.utils.ZipUtils; +import com.github.fmeum.rules_jni.RulesJni; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.management.ManagementFactory; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +/** + * The libFuzzer-compatible CLI entrypoint for Jazzer. + * + * <p>Arguments to Jazzer are passed as command-line arguments or {@code jazzer.*} system + * properties. For example, setting the property {@code jazzer.target_class} to + * {@code com.example.FuzzTest} is equivalent to passing the argument + * {@code --target_class=com.example.FuzzTest}. + * + * <p>Arguments to libFuzzer are passed as command-line arguments. + */ +public class Jazzer { + public static void main(String[] args) throws IOException, InterruptedException { + start(Arrays.stream(args).collect(toList())); + } + + // Accessed by jazzer_main.cpp. + @SuppressWarnings("unused") + private static void main(byte[][] nativeArgs) throws IOException, InterruptedException { + start(Arrays.stream(nativeArgs) + .map(bytes -> new String(bytes, StandardCharsets.UTF_8)) + .collect(toList())); + } + + private static void start(List<String> args) throws IOException, InterruptedException { + // Lock in the output PrintStreams so that Jazzer can still emit output even if the fuzz target + // itself is "silenced" by redirecting System.out and/or System.err. + Log.fixOutErr(System.out, System.err); + + parseJazzerArgsToProperties(args); + + // --asan and --ubsan imply --native by default, but --native can also be used by itself to fuzz + // native libraries without sanitizers (e.g. to quickly grow a corpus). + final boolean loadASan = Boolean.parseBoolean(System.getProperty("jazzer.asan", "false")); + final boolean loadUBSan = Boolean.parseBoolean(System.getProperty("jazzer.ubsan", "false")); + final boolean loadHWASan = Boolean.parseBoolean(System.getProperty("jazzer.hwasan", "false")); + final boolean fuzzNative = Boolean.parseBoolean( + System.getProperty("jazzer.native", Boolean.toString(loadASan || loadUBSan || loadHWASan))); + if ((loadASan || loadUBSan || loadHWASan) && !fuzzNative) { + Log.error("--asan, --hwasan and --ubsan cannot be used without --native"); + exit(1); + } + // No native fuzzing has been requested, fuzz in the current process. + if (!fuzzNative) { + if (IS_ANDROID) { + final String initOptions = getAndroidRuntimeOptions(); + AndroidRuntime.initialize(initOptions); + } + // We only create a wrapper script if libFuzzer runs in a mode that creates subprocesses. + // In LibFuzzer's fork mode, the subprocesses created continuously by the main libFuzzer + // process do not create further subprocesses. Creating a wrapper script for each subprocess + // is an unnecessary overhead. + final boolean spawnsSubprocesses = args.stream().anyMatch(arg + -> (arg.startsWith("-fork=") && !arg.equals("-fork=0")) + || (arg.startsWith("-jobs=") && !arg.equals("-jobs=0")) + || (arg.startsWith("-merge=") && !arg.equals("-merge=0"))); + // argv0 is printed by libFuzzer during reproduction, so have it contain "jazzer". + String arg0 = spawnsSubprocesses ? prepareArgv0(new HashMap<>()) : "jazzer"; + args = Stream.concat(Stream.of(arg0), args.stream()).collect(toList()); + exit(Driver.start(args, spawnsSubprocesses)); + } + + if (!isLinux() && !isMacOs()) { + Log.error("--asan, --ubsan, and --native are only supported on Linux and macOS"); + exit(1); + } + + // Run ourselves as a subprocess with `jazzer_preload` and (optionally) native sanitizers + // preloaded. By inheriting IO, this wrapping should become invisible for the user. + Set<String> argsToFilter = + Stream.of("--asan", "--ubsan", "--hwasan", "--native").collect(toSet()); + ProcessBuilder processBuilder = new ProcessBuilder(); + List<Path> preloadLibs = new ArrayList<>(); + // We have to load jazzer_preload before we load ASan since the ASan includes no-op definitions + // of the fuzzer callbacks as weak symbols, but the dynamic linker doesn't distinguish between + // strong and weak symbols. + preloadLibs.add(RulesJni.extractLibrary("jazzer_preload", Jazzer.class)); + if (loadASan) { + processBuilder.environment().compute("ASAN_OPTIONS", + (name, currentValue) + -> appendWithPathListSeparator(name, + // The JVM produces an extremely large number of false positive leaks, which makes + // it impossible to use LeakSanitizer. + // TODO: Investigate whether we can hook malloc/free only for JNI shared + // libraries, not the JVM itself. + "detect_leaks=0", + // We load jazzer_preload first. + "verify_asan_link_order=0")); + Log.warn("Jazzer is not compatible with LeakSanitizer. Leaks are not reported."); + preloadLibs.add(findLibrary(asanLibNames())); + } + if (loadHWASan) { + processBuilder.environment().compute("HWASAN_OPTIONS", + (name, currentValue) + -> appendWithPathListSeparator(name, + // The JVM produces an extremely large number of false positive leaks, which makes + // it impossible to use LeakSanitizer. + // TODO: Investigate whether we can hook malloc/free only for JNI shared + // libraries, not the JVM itself. + "detect_leaks=0", + // We load jazzer_preload first. + "verify_asan_link_order=0")); + Log.warn("Jazzer is not compatible with LeakSanitizer. Leaks are not reported."); + preloadLibs.add(findLibrary(hwasanLibNames())); + } + if (loadUBSan) { + preloadLibs.add(findLibrary(ubsanLibNames())); + } + // The launcher script we generate is executed by /bin/sh on macOS, which is codesigned without + // the allow-dyld-environment-variables entitlement. The dynamic linker would thus remove all + // DYLD_* variables. Instead, we pass these variables directly to the java executable by + // emitting them into the wrapper. The java binary has both the allow-dyld-environment-variables + // and the disable-library-validation entitlement, which allows any codesigned library to be + // preloaded. + processBuilder.environment().remove(preloadVariable()); + Map<String, String> additionalEnvironment = new HashMap<>(); + additionalEnvironment.put(preloadVariable(), + appendWithPathListSeparator( + preloadVariable(), preloadLibs.stream().map(Path::toString).toArray(String[] ::new))); + List<String> subProcessArgs = + Stream + .concat(Stream.of(prepareArgv0(additionalEnvironment)), + // Prevent a "fork bomb" by stripping all args that trigger this code path. + args.stream().filter(arg -> !argsToFilter.contains(arg.split("=")[0]))) + .collect(toList()); + processBuilder.command(subProcessArgs); + processBuilder.inheritIO(); + + exit(processBuilder.start().waitFor()); + } + + private static void parseJazzerArgsToProperties(List<String> args) { + args.stream() + .filter(arg -> arg.startsWith("--")) + .map(arg -> arg.substring("--".length())) + // Filter out "--", which can be used to declare that all further arguments aren't libFuzzer + // arguments. + .filter(arg -> !arg.isEmpty()) + .map(Jazzer::parseSingleArg) + .forEach(e -> System.setProperty("jazzer." + e.getKey(), e.getValue())); + } + + private static SimpleEntry<String, String> parseSingleArg(String arg) { + String[] nameAndValue = arg.split("=", 2); + if (nameAndValue.length == 2) { + // Example: --keep_going=10 --> (keep_going, 10) + return new SimpleEntry<>(nameAndValue[0], nameAndValue[1]); + } else if (nameAndValue[0].startsWith("no")) { + // Example: --nohooks --> (hooks, "false") + return new SimpleEntry<>(nameAndValue[0].substring("no".length()), "false"); + } else { + // Example: --dedup --> (dedup, "true") + return new SimpleEntry<>(nameAndValue[0], "true"); + } + } + + // Create a wrapper script that faithfully recreates the current JVM. By using this script as + // libFuzzer's argv[0], libFuzzer modes that rely on subprocesses can work with the Java driver. + // This trick is also used to allow native sanitizers to be preloaded. + private static String prepareArgv0(Map<String, String> additionalEnvironment) throws IOException { + if (!isPosixOrAndroid() && !additionalEnvironment.isEmpty()) { + throw new IllegalArgumentException( + "Setting environment variables in the wrapper is only supported on POSIX systems and Android"); + } + char shellQuote = isPosixOrAndroid() ? '\'' : '"'; + String launcherTemplate; + if (IS_ANDROID) { + launcherTemplate = "#!/system/bin/env sh\n%s LD_LIBRARY_PATH=%s \n%s $@\n"; + } else if (isPosix()) { + launcherTemplate = "#!/usr/bin/env sh\n%s $@\n"; + } else { + launcherTemplate = "@echo off\r\n%s %%*\r\n"; + } + + String launcherExtension = isPosix() ? ".sh" : ".bat"; + FileAttribute<?>[] launcherScriptAttributes = isPosixOrAndroid() + ? new FileAttribute[] {PosixFilePermissions.asFileAttribute( + PosixFilePermissions.fromString("rwx------"))} + : new FileAttribute[] {}; + String env = additionalEnvironment.entrySet() + .stream() + .map(e -> e.getKey() + "='" + e.getValue() + "'") + .collect(joining(" ")); + String command = + Stream + .concat(Stream.of(IS_ANDROID ? "exec" : javaBinary().toString()), javaBinaryArgs()) + // Escape individual arguments for the shell. + .map(str -> shellQuote + str + shellQuote) + .collect(joining(" ")); + + String invocation = env.isEmpty() ? command : env + " " + command; + + // argv0 is printed by libFuzzer during reproduction, so have the launcher basename contain + // "jazzer". + Path launcher; + String launcherContent; + if (IS_ANDROID) { + String exportCommand = AndroidRuntime.getClassPathsCommand(); + String ldLibraryPath = AndroidRuntime.getLdLibraryPath(); + launcherContent = String.format(launcherTemplate, exportCommand, ldLibraryPath, invocation); + launcher = Files.createTempFile( + Paths.get("/data/local/tmp/"), "jazzer-", launcherExtension, launcherScriptAttributes); + } else { + launcherContent = String.format(launcherTemplate, invocation); + launcher = Files.createTempFile("jazzer-", launcherExtension, launcherScriptAttributes); + } + + launcher.toFile().deleteOnExit(); + Files.write(launcher, launcherContent.getBytes(StandardCharsets.UTF_8)); + return launcher.toAbsolutePath().toString(); + } + + private static Path javaBinary() { + String javaBinaryName; + if (isPosix()) { + javaBinaryName = "java"; + } else { + javaBinaryName = "java.exe"; + } + + return Paths.get(System.getProperty("java.home"), "bin", javaBinaryName); + } + + private static Stream<String> javaBinaryArgs() throws IOException { + if (IS_ANDROID) { + // Add Android specific args + Path agentPath = + RulesJni.extractLibrary("android_native_agent", "/com/code_intelligence/jazzer/android"); + + String jazzerAgentPath = System.getProperty("jazzer.agent_path"); + String bootclassClassOverrides = + System.getProperty("jazzer.android_bootpath_classes_overrides"); + + String jazzerBootstrapJarPath = + "com/code_intelligence/jazzer/android/jazzer_bootstrap_android.jar"; + String jazzerBootstrapJarOut = "/data/local/tmp/jazzer_bootstrap_android.jar"; + + try { + ZipUtils.extractFile(jazzerAgentPath, jazzerBootstrapJarPath, jazzerBootstrapJarOut); + } catch (IOException ioe) { + Log.error( + "Could not extract jazzer_bootstrap_android.jar from Jazzer standalone agent", ioe); + exit(1); + } + + String nativeAgentOptions = "injectJars=" + jazzerBootstrapJarOut; + if (bootclassClassOverrides != null && !bootclassClassOverrides.isEmpty()) { + nativeAgentOptions += ",bootstrapClassOverrides=" + bootclassClassOverrides; + } + + // ManagementFactory wont work with Android + Stream<String> stream = Stream.of("app_process", "-Djdk.attach.allowAttachSelf=true", + "-Xplugin:libopenjdkjvmti.so", + "-agentpath:" + agentPath.toString() + "=" + nativeAgentOptions, "-Xcompiler-option", + "--debuggable", "/system/bin", Jazzer.class.getName()); + + return stream; + } + + Stream<String> stream = Stream.of("-cp", System.getProperty("java.class.path"), + // Make ByteBuddyAgent's job simpler by allowing it to attach directly to the JVM + // rather than relying on an external helper. The latter fails on macOS 12 with JDK 11+ + // (but not 8) and UBSan preloaded with: + // Caused by: java.io.IOException: Cannot run program + // "/Users/runner/hostedtoolcache/Java_Zulu_jdk/17.0.4-8/x64/bin/java": error=0, Failed + // to exec spawn helper: pid: 8227, signal: 9 + // Presumably, this issue is caused by codesigning and the exec helper missing the + // entitlements required for library insertion. + "-Djdk.attach.allowAttachSelf=true", Jazzer.class.getName()); + + return Stream.concat(ManagementFactory.getRuntimeMXBean().getInputArguments().stream(), stream); + } + + /** + * Append the given elements to the value of the environment variable {@code name} that contains a + * list of paths separated by the system path list separator. + */ + private static String appendWithPathListSeparator(String name, String... options) { + if (options.length == 0) { + throw new IllegalArgumentException("options must not be empty"); + } + + String currentValue = Optional.ofNullable(System.getenv(name)).orElse(""); + String additionalOptions = String.join(File.pathSeparator, options); + if (currentValue.isEmpty()) { + return additionalOptions; + } + return currentValue + File.pathSeparator + additionalOptions; + } + + private static Path findLibrary(List<String> candidateNames) { + if (!IS_ANDROID) { + return findHostClangLibrary(candidateNames); + } + + for (String candidateName : candidateNames) { + String candidateFullPath = "/apex/com.android.runtime/lib64/bionic/" + candidateName; + File f = new File(candidateFullPath); + if (f.exists()) { + return Paths.get(candidateFullPath); + } + } + + Log.error( + String.format("Failed to find one of %s%n for Android", String.join(", ", candidateNames))); + Log.error("If fuzzing hwasan, make sure you have a hwasan build flashed to your device"); + + exit(1); + throw new IllegalStateException("not reached"); + } + + private static Path findHostClangLibrary(List<String> candidateNames) { + for (String name : candidateNames) { + Optional<Path> path = tryFindLibraryInJazzerNativeSanitizersDir(name); + if (path.isPresent()) { + return path.get(); + } + } + for (String name : candidateNames) { + Optional<Path> path = tryFindLibraryUsingClang(name); + if (path.isPresent()) { + return path.get(); + } + } + Log.error("Failed to find one of: " + String.join(", ", candidateNames)); + exit(1); + throw new IllegalStateException("not reached"); + } + + private static Optional<Path> tryFindLibraryInJazzerNativeSanitizersDir(String name) { + String nativeSanitizersDir = System.getenv("JAZZER_NATIVE_SANITIZERS_DIR"); + if (nativeSanitizersDir == null) { + return Optional.empty(); + } + Path candidatePath = Paths.get(nativeSanitizersDir, name); + if (Files.exists(candidatePath)) { + return Optional.of(candidatePath); + } else { + return Optional.empty(); + } + } + + /** + * Given a library name such as "libclang_rt.asan-x86_64.so", get the full path to the library + * installed on the host from clang (or CC, if set). Returns Optional.empty() if clang does not + * find the library and exits with a message in case of any other error condition. + */ + private static Optional<Path> tryFindLibraryUsingClang(String name) { + List<String> command = asList(hostClang(), "--print-file-name", name); + ProcessBuilder processBuilder = new ProcessBuilder(command); + byte[] output; + try { + Process process = processBuilder.start(); + if (process.waitFor() != 0) { + Log.error(String.format( + "'%s' exited with exit code %d", String.join(" ", command), process.exitValue())); + copy(process.getInputStream(), System.out); + copy(process.getErrorStream(), System.err); + exit(1); + } + output = readAllBytes(process.getInputStream()); + } catch (IOException | InterruptedException e) { + Log.error(String.format("Failed to run '%s'", String.join(" ", command)), e); + exit(1); + throw new IllegalStateException("not reached"); + } + Path library = Paths.get(new String(output).trim()); + if (Files.exists(library)) { + return Optional.of(library); + } + return Optional.empty(); + } + + private static String hostClang() { + return Optional.ofNullable(System.getenv("CC")).orElse("clang"); + } + + private static List<String> hwasanLibNames() { + if (!IS_ANDROID) { + Log.error("HWAsan is only supported for Android. Please try --asan"); + exit(1); + } + + return singletonList("libclang_rt.hwasan-aarch64-android.so"); + } + + private static List<String> asanLibNames() { + if (isLinux()) { + if (IS_ANDROID) { + Log.error("ASan is not supported for Android at this time. Use --hwasan for Address " + + "Sanitization on Android"); + exit(1); + } + + // Since LLVM 15 sanitizer runtimes no longer have the architecture in the filename. + return asList("libclang_rt.asan.so", "libclang_rt.asan-x86_64.so"); + } else { + return singletonList("libclang_rt.asan_osx_dynamic.dylib"); + } + } + + private static List<String> ubsanLibNames() { + if (isLinux()) { + if (IS_ANDROID) { + // return asList("libclang_rt.ubsan_standalone-aarch64-android.so"); + Log.error("ERROR: UBSan is not supported for Android at this time."); + exit(1); + } + + return asList("libclang_rt.ubsan_standalone.so", "libclang_rt.ubsan_standalone-x86_64.so"); + } else { + return singletonList("libclang_rt.ubsan_osx_dynamic.dylib"); + } + } + + private static String preloadVariable() { + return isLinux() ? "LD_PRELOAD" : "DYLD_INSERT_LIBRARIES"; + } + + private static boolean isLinux() { + return System.getProperty("os.name").startsWith("Linux"); + } + + private static boolean isMacOs() { + return System.getProperty("os.name").startsWith("Mac OS X"); + } + + private static boolean isPosix() { + return !IS_ANDROID && FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); + } + + private static String getAndroidRuntimeOptions() { + List<String> validInitOptions = Arrays.asList("use_platform_libs", "use_none", ""); + String initOptString = System.getProperty("jazzer.android_init_options"); + if (!validInitOptions.contains(initOptString)) { + Log.error("Invalid android_init_options set for Android Runtime."); + exit(1); + } + return initOptString; + } + + private static boolean isPosixOrAndroid() { + if (isPosix()) { + return true; + } + return IS_ANDROID; + } + + private static byte[] readAllBytes(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + copy(in, out); + return out.toByteArray(); + } + + private static void copy(InputStream source, OutputStream target) throws IOException { + byte[] buffer = new byte[64 * 104 * 1024]; + int read; + while ((read = source.read(buffer)) != -1) { + target.write(buffer, 0, read); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt b/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt new file mode 100644 index 00000000..9bcd744f --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt @@ -0,0 +1,172 @@ +// Copyright 2021 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. + +@file:JvmName("Agent") + +package com.code_intelligence.jazzer.agent + +import com.code_intelligence.jazzer.driver.Opt +import com.code_intelligence.jazzer.instrumentor.CoverageRecorder +import com.code_intelligence.jazzer.instrumentor.Hooks +import com.code_intelligence.jazzer.instrumentor.InstrumentationType +import com.code_intelligence.jazzer.sanitizers.Constants +import com.code_intelligence.jazzer.utils.ClassNameGlobber +import com.code_intelligence.jazzer.utils.Log +import com.code_intelligence.jazzer.utils.ManifestUtils +import java.lang.instrument.Instrumentation +import java.nio.file.Paths +import kotlin.io.path.exists +import kotlin.io.path.isDirectory + +fun install(instrumentation: Instrumentation) { + installInternal(instrumentation) +} + +fun installInternal( + instrumentation: Instrumentation, + userHookNames: List<String> = findManifestCustomHookNames() + Opt.customHooks, + disabledHookNames: List<String> = Opt.disabledHooks, + instrumentationIncludes: List<String> = Opt.instrumentationIncludes.get(), + instrumentationExcludes: List<String> = Opt.instrumentationExcludes.get(), + customHookIncludes: List<String> = Opt.customHookIncludes.get(), + customHookExcludes: List<String> = Opt.customHookExcludes.get(), + trace: List<String> = Opt.trace, + idSyncFile: String? = Opt.idSyncFile, + dumpClassesDir: String = Opt.dumpClassesDir, + additionalClassesExcludes: List<String> = Opt.additionalClassesExcludes, +) { + val allCustomHookNames = (Constants.SANITIZER_HOOK_NAMES + userHookNames).toSet() + check(allCustomHookNames.isNotEmpty()) { "No hooks registered; expected at least the built-in hooks" } + val customHookNames = allCustomHookNames - disabledHookNames.toSet() + val disabledCustomHooksToPrint = allCustomHookNames - customHookNames.toSet() + if (disabledCustomHooksToPrint.isNotEmpty()) { + Log.info("Not using the following disabled hooks: ${disabledCustomHooksToPrint.joinToString(", ")}") + } + + val classNameGlobber = ClassNameGlobber(instrumentationIncludes, instrumentationExcludes + customHookNames) + CoverageRecorder.classNameGlobber = classNameGlobber + val customHookClassNameGlobber = ClassNameGlobber(customHookIncludes, customHookExcludes + customHookNames) + // FIXME: Setting trace to the empty string explicitly results in all rather than no trace types + // being applied - this is unintuitive. + val instrumentationTypes = (trace.takeIf { it.isNotEmpty() } ?: listOf("all")).flatMap { + when (it) { + "cmp" -> setOf(InstrumentationType.CMP) + "cov" -> setOf(InstrumentationType.COV) + "div" -> setOf(InstrumentationType.DIV) + "gep" -> setOf(InstrumentationType.GEP) + "indir" -> setOf(InstrumentationType.INDIR) + "native" -> setOf(InstrumentationType.NATIVE) + // Disable GEP instrumentation by default as it appears to negatively affect fuzzing + // performance. Our current GEP instrumentation only reports constant indices, but even + // when we instead reported non-constant indices, they tended to completely fill up the + // table of recent compares and value profile map. + "all" -> InstrumentationType.values().toSet() - InstrumentationType.GEP + else -> { + println("WARN: Skipping unknown instrumentation type $it") + emptySet() + } + } + }.toSet() + + val idSyncFilePath = idSyncFile?.takeUnless { it.isEmpty() }?.let { + Paths.get(it).also { path -> + Log.info("Synchronizing coverage IDs in ${path.toAbsolutePath()}") + } + } + val dumpClassesDirPath = dumpClassesDir.takeUnless { it.isEmpty() }?.let { + Paths.get(it).toAbsolutePath().also { path -> + if (path.exists() && path.isDirectory()) { + Log.info("Dumping instrumented classes into $path") + } else { + Log.error("Cannot dump instrumented classes into $path; does not exist or not a directory") + } + } + } + val includedHookNames = instrumentationTypes + .mapNotNull { type -> + when (type) { + InstrumentationType.CMP -> "com.code_intelligence.jazzer.runtime.TraceCmpHooks" + InstrumentationType.DIV -> "com.code_intelligence.jazzer.runtime.TraceDivHooks" + InstrumentationType.INDIR -> "com.code_intelligence.jazzer.runtime.TraceIndirHooks" + InstrumentationType.NATIVE -> "com.code_intelligence.jazzer.runtime.NativeLibHooks" + else -> null + } + } + val coverageIdSynchronizer = if (idSyncFilePath != null) { + FileSyncCoverageIdStrategy(idSyncFilePath) + } else { + MemSyncCoverageIdStrategy() + } + + // If we don't append the JARs containing the custom hooks to the bootstrap class loader, + // third-party hooks not contained in the agent JAR will not be able to instrument Java standard + // library classes. These classes are loaded by the bootstrap / system class loader and would + // not be considered when resolving references to hook methods, leading to NoClassDefFoundError + // being thrown. + Hooks.appendHooksToBootstrapClassLoaderSearch(instrumentation, customHookNames.toSet()) + val (includedHooks, customHooks) = Hooks.loadHooks(additionalClassesExcludes, includedHookNames.toSet(), customHookNames.toSet()) + + val runtimeInstrumentor = RuntimeInstrumentor( + instrumentation, + classNameGlobber, + customHookClassNameGlobber, + instrumentationTypes, + includedHooks.hooks, + customHooks.hooks, + customHooks.additionalHookClassNameGlobber, + coverageIdSynchronizer, + dumpClassesDirPath, + ) + + // These classes are e.g. dependencies of the RuntimeInstrumentor or hooks and thus were loaded + // before the instrumentor was ready. Since we haven't enabled it yet, they can safely be + // "retransformed": They haven't been transformed yet. + val classesToRetransform = instrumentation.allLoadedClasses + .filter { + classNameGlobber.includes(it.name) || + customHookClassNameGlobber.includes(it.name) || + customHooks.additionalHookClassNameGlobber.includes(it.name) + } + .filter { + instrumentation.isModifiableClass(it) + } + .toTypedArray() + + instrumentation.addTransformer(runtimeInstrumentor, true) + + if (classesToRetransform.isNotEmpty()) { + if (instrumentation.isRetransformClassesSupported) { + retransformClassesWithRetry(instrumentation, classesToRetransform) + } + } +} + +private fun retransformClassesWithRetry(instrumentation: Instrumentation, classesToRetransform: Array<Class<*>>) { + try { + instrumentation.retransformClasses(*classesToRetransform) + } catch (e: Throwable) { + if (classesToRetransform.size == 1) { + Log.warn("Error retransforming class ${classesToRetransform[0].name }", e) + } else { + // The docs state that no transformation was performed if an exception is thrown. + // Try again in a binary search fashion, until the not transformable classes have been isolated and reported. + retransformClassesWithRetry(instrumentation, classesToRetransform.copyOfRange(0, classesToRetransform.size / 2)) + retransformClassesWithRetry(instrumentation, classesToRetransform.copyOfRange(classesToRetransform.size / 2, classesToRetransform.size)) + } + } +} + +private fun findManifestCustomHookNames() = ManifestUtils.combineManifestValues(ManifestUtils.HOOK_CLASSES) + .flatMap { it.split(':') } + .filter { it.isNotBlank() } diff --git a/src/main/java/com/code_intelligence/jazzer/agent/AgentInstaller.java b/src/main/java/com/code_intelligence/jazzer/agent/AgentInstaller.java new file mode 100644 index 00000000..5dd041ac --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/agent/AgentInstaller.java @@ -0,0 +1,58 @@ +// Copyright 2022 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.agent; + +import static com.code_intelligence.jazzer.agent.AgentUtils.extractBootstrapJar; +import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID; + +import java.lang.instrument.Instrumentation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicBoolean; +import net.bytebuddy.agent.ByteBuddyAgent; + +public class AgentInstaller { + private static final AtomicBoolean hasBeenInstalled = new AtomicBoolean(); + + /** + * Appends the parts of Jazzer that have to be visible to all classes, including those in the Java + * standard library, to the bootstrap class loader path. Additionally, if enableAgent is true, + * also enables the Jazzer agent that instruments classes for fuzzing. + */ + public static void install(boolean enableAgent) { + // Only install the agent once. + if (!hasBeenInstalled.compareAndSet(false, true)) { + return; + } + + if (IS_ANDROID) { + return; + } + + Instrumentation instrumentation = ByteBuddyAgent.install(); + instrumentation.appendToBootstrapClassLoaderSearch(extractBootstrapJar()); + if (!enableAgent) { + return; + } + try { + Class<?> agent = Class.forName("com.code_intelligence.jazzer.agent.Agent"); + Method install = agent.getMethod("install", Instrumentation.class); + install.invoke(null, instrumentation); + } catch (ClassNotFoundException | InvocationTargetException | NoSuchMethodException + | IllegalAccessException e) { + throw new IllegalStateException("Failed to run Agent.install", e); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/agent/AgentUtils.java b/src/main/java/com/code_intelligence/jazzer/agent/AgentUtils.java new file mode 100644 index 00000000..e654252a --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/agent/AgentUtils.java @@ -0,0 +1,45 @@ +/* + * 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.agent; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.jar.JarFile; + +final class AgentUtils { + private static final String BOOTSTRAP_JAR = + "/com/code_intelligence/jazzer/runtime/jazzer_bootstrap.jar"; + + public static JarFile extractBootstrapJar() { + try (InputStream bootstrapJarStream = AgentUtils.class.getResourceAsStream(BOOTSTRAP_JAR)) { + if (bootstrapJarStream == null) { + throw new IllegalStateException("Failed to find Jazzer agent bootstrap jar in resources"); + } + File bootstrapJar = Files.createTempFile("jazzer-agent-", ".jar").toFile(); + bootstrapJar.deleteOnExit(); + Files.copy(bootstrapJarStream, bootstrapJar.toPath(), StandardCopyOption.REPLACE_EXISTING); + return new JarFile(bootstrapJar); + } catch (IOException e) { + throw new IllegalStateException("Failed to extract Jazzer agent bootstrap jar", e); + } + } + + private AgentUtils() {} +} diff --git a/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel new file mode 100644 index 00000000..89acbda3 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel @@ -0,0 +1,43 @@ +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") +load("//bazel:kotlin.bzl", "ktlint") + +java_library( + name = "agent_installer", + srcs = ["AgentInstaller.java"], + resources = select({ + "@platforms//os:android": [ + "//src/main/java/com/code_intelligence/jazzer/android:jazzer_bootstrap_android", + ], + "//conditions:default": [ + "//src/main/java/com/code_intelligence/jazzer/runtime:jazzer_bootstrap", + ], + }), + visibility = ["//visibility:public"], + deps = [ + ":agent_lib", + "//src/main/java/com/code_intelligence/jazzer/driver:opt", + "//src/main/java/com/code_intelligence/jazzer/runtime:constants", + "@net_bytebuddy_byte_buddy_agent//jar", + ], +) + +kt_jvm_library( + name = "agent_lib", + srcs = [ + "Agent.kt", + "AgentUtils.java", + "CoverageIdStrategy.kt", + "RuntimeInstrumentor.kt", + ], + deps = [ + "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers:constants", + "//src/main/java/com/code_intelligence/jazzer/driver:opt", + "//src/main/java/com/code_intelligence/jazzer/instrumentor", + "//src/main/java/com/code_intelligence/jazzer/utils:class_name_globber", + "//src/main/java/com/code_intelligence/jazzer/utils:log", + "//src/main/java/com/code_intelligence/jazzer/utils:manifest_utils", + "@com_github_classgraph_classgraph//:classgraph", + ], +) + +ktlint() diff --git a/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt b/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt new file mode 100644 index 00000000..75d76003 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt @@ -0,0 +1,223 @@ +// Copyright 2021 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.agent + +import com.code_intelligence.jazzer.utils.Log +import java.nio.ByteBuffer +import java.nio.channels.FileChannel +import java.nio.channels.FileLock +import java.nio.file.Path +import java.nio.file.StandardOpenOption +import java.util.UUID + +/** + * Indicates a fatal failure to generate synchronized coverage IDs. + */ +class CoverageIdException(cause: Throwable? = null) : + RuntimeException("Failed to synchronize coverage IDs", cause) + +/** + * [CoverageIdStrategy] provides an abstraction to switch between context specific coverage ID generation. + * + * Coverage (i.e., edge) IDs differ from other kinds of IDs, such as those generated for call sites or cmp + * instructions, in that they should be consecutive, collision-free, and lie in a known, small range. + * This precludes us from generating them simply as hashes of class names. + */ +interface CoverageIdStrategy { + + /** + * [withIdForClass] provides the initial coverage ID of the given [className] as parameter to the + * [block] to execute. [block] has to return the number of additionally used IDs. + */ + @Throws(CoverageIdException::class) + fun withIdForClass(className: String, block: (Int) -> Int) +} + +/** + * A memory synced strategy for coverage ID generation. + * + * This strategy uses a synchronized block to guard access to a global edge ID counter. + * Even though concurrent fuzzing is not fully supported this strategy enables consistent coverage + * IDs in case of concurrent class loading. + * + * It only prevents races within one VM instance. + */ +class MemSyncCoverageIdStrategy : CoverageIdStrategy { + private var nextEdgeId = 0 + + @Synchronized + override fun withIdForClass(className: String, block: (Int) -> Int) { + nextEdgeId += block(nextEdgeId) + } +} + +/** + * A strategy for coverage ID generation that synchronizes the IDs assigned to a class with other processes via the + * specified [idSyncFile]. + * This class takes care of synchronizing the access to the file between multiple processes as long as the general + * contract of [CoverageIdStrategy] is followed. + */ +class FileSyncCoverageIdStrategy(private val idSyncFile: Path) : CoverageIdStrategy { + private val uuid: UUID = UUID.randomUUID() + private var idFileLock: FileLock? = null + + private var cachedFirstId: Int? = null + private var cachedClassName: String? = null + private var cachedIdCount: Int? = null + + /** + * This method is synchronized to prevent concurrent access to the internal file lock which would result in + * [java.nio.channels.OverlappingFileLockException]. Furthermore, every coverage ID obtained by [obtainFirstId] + * is always committed back again to the sync file by [commitIdCount]. + */ + @Synchronized + override fun withIdForClass(className: String, block: (Int) -> Int) { + var actualNumEdgeIds = 0 + try { + val firstId = obtainFirstId(className) + actualNumEdgeIds = block(firstId) + } finally { + commitIdCount(actualNumEdgeIds) + } + } + + /** + * Obtains a coverage ID for [className] such that all cooperating agent processes will obtain the same ID. + * There are two cases to consider: + * - This agent process is the first to encounter [className], i.e., it does not find a record for that class in + * [idSyncFile]. In this case, a lock on the file is held until the class has been instrumented and a record with + * the required number of coverage IDs has been added. + * - Another agent process has already encountered [className], i.e., there is a record that class in [idSyncFile]. + * In this case, the lock on the file is returned immediately and the extracted first coverage ID is returned to + * the caller. The caller is still expected to call [commitIdCount] so that desynchronization can be detected. + */ + private fun obtainFirstId(className: String): Int { + try { + check(idFileLock == null) { "Already holding a lock on the ID file" } + val localIdFile = FileChannel.open( + idSyncFile, + StandardOpenOption.WRITE, + StandardOpenOption.READ, + ) + // Wait until we have obtained the lock on the sync file. We hold the lock from this point until we have + // finished reading and writing (if necessary) to the file. + val localIdFileLock = localIdFile.lock() + check(localIdFileLock.isValid && !localIdFileLock.isShared) + // Parse the sync file, which consists of lines of the form + // <class name>:<first ID>:<num IDs> + val idInfo = localIdFileLock.channel().readFully() + .lineSequence() + .filterNot { it.isBlank() } + .map { line -> + val parts = line.split(':') + check(parts.size == 4) { + "Expected ID file line to be of the form '<class name>:<first ID>:<num IDs>:<uuid>', got '$line'" + } + val lineClassName = parts[0] + val lineFirstId = parts[1].toInt() + check(lineFirstId >= 0) { "Negative first ID in line: $line" } + val lineIdCount = parts[2].toInt() + check(lineIdCount >= 0) { "Negative ID count in line: $line" } + Triple(lineClassName, lineFirstId, lineIdCount) + }.toList() + cachedClassName = className + val idInfoForClass = idInfo.filter { it.first == className } + return when (idInfoForClass.size) { + 0 -> { + // We are the first to encounter this class and thus need to hold the lock until the class has been + // instrumented and we know the required number of coverage IDs. + idFileLock = localIdFileLock + // Compute the next free ID as the maximum over the sums of first ID and ID count, starting at 0 if + // this is the first ID to be assigned. In fact, since this is the only way new lines are added to + // the file, the maximum is always attained by the last line. + val nextFreeId = idInfo.asSequence().map { it.second + it.third }.lastOrNull() ?: 0 + cachedFirstId = nextFreeId + nextFreeId + } + 1 -> { + // This class has already been instrumented elsewhere, so we just return the first ID and ID count + // reported from there and release the lock right away. The caller is still expected to call + // commitIdCount. + localIdFile.close() + cachedIdCount = idInfoForClass.single().third + idInfoForClass.single().second + } + else -> { + localIdFile.close() + Log.println(idInfo.joinToString("\n") { "${it.first}:${it.second}:${it.third}" }) + throw IllegalStateException("Multiple entries for $className in ID file") + } + } + } catch (e: Exception) { + throw CoverageIdException(e) + } + } + + /** + * Records the number of coverage IDs used to instrument the class specified in a previous call to [obtainFirstId]. + * If instrumenting the class should fail, this function must still be called. In this case, [idCount] is set to 0. + */ + private fun commitIdCount(idCount: Int) { + val localIdFileLock = idFileLock + try { + check(cachedClassName != null) + if (localIdFileLock == null) { + // We released the lock already in obtainFirstId since the class had already been instrumented + // elsewhere. As we know the expected number of IDs for the current class in this case, check for + // deviations. + check(cachedIdCount != null) + check(idCount == cachedIdCount) { + "$cachedClassName has $idCount edges, but $cachedIdCount edges reserved in ID file" + } + } else { + // We are the first to instrument this class and should record the number of IDs in the sync file. + check(cachedFirstId != null) + localIdFileLock.channel().append("$cachedClassName:$cachedFirstId:$idCount:$uuid\n") + localIdFileLock.channel().force(true) + } + idFileLock = null + cachedFirstId = null + cachedIdCount = null + cachedClassName = null + } catch (e: Exception) { + throw CoverageIdException(e) + } finally { + localIdFileLock?.channel()?.close() + } + } +} + +/** + * Reads the [FileChannel] to the end as a UTF-8 string. + */ +fun FileChannel.readFully(): String { + check(size() <= Int.MAX_VALUE) + val buffer = ByteBuffer.allocate(size().toInt()) + while (buffer.hasRemaining()) { + when (read(buffer)) { + 0 -> throw IllegalStateException("No bytes read") + -1 -> break + } + } + return String(buffer.array()) +} + +/** + * Appends [string] to the end of the [FileChannel]. + */ +fun FileChannel.append(string: String) { + position(size()) + write(ByteBuffer.wrap(string.toByteArray())) +} diff --git a/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt new file mode 100644 index 00000000..57410f30 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt @@ -0,0 +1,243 @@ +// Copyright 2021 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.agent + +import com.code_intelligence.jazzer.driver.Opt +import com.code_intelligence.jazzer.instrumentor.ClassInstrumentor +import com.code_intelligence.jazzer.instrumentor.CoverageRecorder +import com.code_intelligence.jazzer.instrumentor.Hook +import com.code_intelligence.jazzer.instrumentor.InstrumentationType +import com.code_intelligence.jazzer.utils.ClassNameGlobber +import com.code_intelligence.jazzer.utils.Log +import io.github.classgraph.ClassGraph +import java.io.File +import java.lang.instrument.ClassFileTransformer +import java.lang.instrument.Instrumentation +import java.nio.file.Path +import java.security.ProtectionDomain +import kotlin.math.roundToInt +import kotlin.system.exitProcess +import kotlin.time.measureTimedValue + +class RuntimeInstrumentor( + private val instrumentation: Instrumentation, + private val classesToFullyInstrument: ClassNameGlobber, + private val classesToHookInstrument: ClassNameGlobber, + private val instrumentationTypes: Set<InstrumentationType>, + private val includedHooks: List<Hook>, + private val customHooks: List<Hook>, + // Dedicated name globber for additional classes to hook stated in hook annotations is needed due to + // existing include and exclude pattern of classesToHookInstrument. All classes are included in hook + // instrumentation except the ones from default excludes, like JDK and Kotlin classes. But additional + // classes to hook, based on annotations, are allowed to reference normally ignored ones, like JDK + // and Kotlin internals. + // FIXME: Adding an additional class to hook will apply _all_ hooks to it and not only the one it's + // defined in. At some point we might want to track the list of classes per custom hook rather than globally. + private val additionalClassesToHookInstrument: ClassNameGlobber, + private val coverageIdSynchronizer: CoverageIdStrategy, + private val dumpClassesDir: Path?, +) : ClassFileTransformer { + + @kotlin.time.ExperimentalTime + override fun transform( + loader: ClassLoader?, + internalClassName: String, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain?, + classfileBuffer: ByteArray, + ): ByteArray? { + var pathPrefix = "" + if (!Opt.instrumentOnly.isEmpty() && protectionDomain != null) { + var outputPathPrefix = protectionDomain.getCodeSource().getLocation().getFile().toString() + if (outputPathPrefix.isNotEmpty()) { + if (outputPathPrefix.contains(File.separator)) { + outputPathPrefix = outputPathPrefix.substring(outputPathPrefix.lastIndexOf(File.separator) + 1, outputPathPrefix.length) + } + + if (outputPathPrefix.endsWith(".jar")) { + outputPathPrefix = outputPathPrefix.substring(0, outputPathPrefix.lastIndexOf(".jar")) + } + + if (outputPathPrefix.isNotEmpty()) { + pathPrefix = outputPathPrefix + File.separator + } + } + } + + return try { + // Bail out early if we would instrument ourselves. This prevents ClassCircularityErrors as we might need to + // load additional Jazzer classes until we reach the full exclusion logic. + if (internalClassName.startsWith("com/code_intelligence/jazzer/")) { + return null + } + // Workaround for a JDK bug (http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8299798): + // When retransforming a class in the Java standard library, the provided classfileBuffer does not contain + // any StackMapTable attributes. Our transformations require stack map frames to calculate the number of + // local variables and stack slots as well as when adding control flow. + // + // We work around this by reloading the class file contents if we are retransforming (classBeingRedefined + // is also non-null in this situation) and the class is provided by the bootstrap loader. + // + // Alternatives considered: + // Using ClassWriter.COMPUTE_FRAMES as an escape hatch isn't possible in the context of an agent as the + // computation may itself need to load classes, which leads to circular loads and incompatible class + // redefinitions. + transformInternal(internalClassName, classfileBuffer.takeUnless { loader == null && classBeingRedefined != null }) + } catch (t: Throwable) { + // Throwables raised from transform are silently dropped, making it extremely hard to detect instrumentation + // failures. The docs advise to use a top-level try-catch. + // https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/ClassFileTransformer.html + if (dumpClassesDir != null) { + dumpToClassFile(internalClassName, classfileBuffer, basenameSuffix = ".failed", pathPrefix = pathPrefix) + } + Log.warn("Failed to instrument $internalClassName:", t) + throw t + }.also { instrumentedByteCode -> + // Only dump classes that were instrumented. + if (instrumentedByteCode != null && dumpClassesDir != null) { + dumpToClassFile(internalClassName, instrumentedByteCode, pathPrefix = pathPrefix) + dumpToClassFile(internalClassName, classfileBuffer, basenameSuffix = ".original", pathPrefix = pathPrefix) + } + } + } + + private fun dumpToClassFile(internalClassName: String, bytecode: ByteArray, basenameSuffix: String = "", pathPrefix: String = "") { + val relativePath = "$pathPrefix$internalClassName$basenameSuffix.class" + val absolutePath = dumpClassesDir!!.resolve(relativePath) + val dumpFile = absolutePath.toFile() + dumpFile.parentFile.mkdirs() + dumpFile.writeBytes(bytecode) + } + + @kotlin.time.ExperimentalTime + override fun transform( + module: Module?, + loader: ClassLoader?, + internalClassName: String, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain?, + classfileBuffer: ByteArray, + ): ByteArray? { + try { + if (module != null && !module.canRead(RuntimeInstrumentor::class.java.module)) { + // Make all other modules read our (unnamed) module, which allows them to access the classes needed by the + // instrumentations, e.g. CoverageMap. If a module can't be modified, it should not be instrumented as the + // injected bytecode might throw NoClassDefFoundError. + // https://mail.openjdk.java.net/pipermail/jigsaw-dev/2021-May/014663.html + if (!instrumentation.isModifiableModule(module)) { + val prettyClassName = internalClassName.replace('/', '.') + Log.warn("Failed to instrument $prettyClassName in unmodifiable module ${module.name}, skipping") + return null + } + instrumentation.redefineModule( + module, + setOf(RuntimeInstrumentor::class.java.module), // extraReads + emptyMap(), + emptyMap(), + emptySet(), + emptyMap(), + ) + } + } catch (t: Throwable) { + // Throwables raised from transform are silently dropped, making it extremely hard to detect instrumentation + // failures. The docs advise to use a top-level try-catch. + // https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/ClassFileTransformer.html + if (dumpClassesDir != null) { + dumpToClassFile(internalClassName, classfileBuffer, basenameSuffix = ".failed") + } + Log.warn("Failed to instrument $internalClassName:", t) + throw t + } + return transform(loader, internalClassName, classBeingRedefined, protectionDomain, classfileBuffer) + } + + @kotlin.time.ExperimentalTime + fun transformInternal(internalClassName: String, maybeClassfileBuffer: ByteArray?): ByteArray? { + val (fullInstrumentation, printInfo) = when { + classesToFullyInstrument.includes(internalClassName) -> Pair(true, true) + classesToHookInstrument.includes(internalClassName) -> Pair(false, true) + // The classes to hook specified by hooks are more of an implementation detail of the hook. The list is + // always the same unless the set of hooks changes and doesn't help the user judge whether their classes are + // being instrumented, so we don't print info for them. + additionalClassesToHookInstrument.includes(internalClassName) -> Pair(false, false) + else -> return null + } + val className = internalClassName.replace('/', '.') + val classfileBuffer = maybeClassfileBuffer ?: ClassGraph() + .enableSystemJarsAndModules() + .ignoreClassVisibility() + .acceptClasses(className) + .scan() + .use { + it.getClassInfo(className)?.resource?.load() ?: run { + Log.warn("Failed to load bytecode of class $className") + return null + } + } + val (instrumentedBytecode, duration) = measureTimedValue { + try { + instrument(internalClassName, classfileBuffer, fullInstrumentation) + } catch (e: CoverageIdException) { + Log.error("Coverage IDs are out of sync") + e.printStackTrace() + exitProcess(1) + } + } + val durationInMs = duration.inWholeMilliseconds + val sizeIncrease = ((100.0 * (instrumentedBytecode.size - classfileBuffer.size)) / classfileBuffer.size).roundToInt() + if (printInfo) { + if (fullInstrumentation) { + Log.info("Instrumented $className (took $durationInMs ms, size +$sizeIncrease%)") + } else { + Log.info("Instrumented $className with custom hooks only (took $durationInMs ms, size +$sizeIncrease%)") + } + } + return instrumentedBytecode + } + + private fun instrument(internalClassName: String, bytecode: ByteArray, fullInstrumentation: Boolean): ByteArray { + val classWithHooksEnabledField = if (Opt.conditionalHooks) { + // Let the hook instrumentation emit additional logic that checks the value of the + // hooksEnabled field on this class and skips the hook if it is false. + "com/code_intelligence/jazzer/runtime/JazzerInternal" + } else { + null + } + return ClassInstrumentor(internalClassName, bytecode).run { + if (fullInstrumentation) { + // Coverage instrumentation must be performed before any other code updates + // or there will be additional coverage points injected if any calls are inserted + // and JaCoCo will produce a broken coverage report. + coverageIdSynchronizer.withIdForClass(internalClassName) { firstId -> + coverage(firstId).also { actualNumEdgeIds -> + CoverageRecorder.recordInstrumentedClass( + internalClassName, + bytecode, + firstId, + actualNumEdgeIds, + ) + } + } + // Hook instrumentation must be performed after data flow tracing as the injected + // bytecode would trigger the GEP callbacks for byte[]. + traceDataFlow(instrumentationTypes) + hooks(includedHooks + customHooks, classWithHooksEnabledField) + } else { + hooks(customHooks, classWithHooksEnabledField) + } + instrumentedBytecode + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/android/AndroidRuntime.java b/src/main/java/com/code_intelligence/jazzer/android/AndroidRuntime.java new file mode 100644 index 00000000..3a80c314 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/android/AndroidRuntime.java @@ -0,0 +1,67 @@ +// Copyright 2021 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.android; + +import com.code_intelligence.jazzer.utils.Log; +import com.github.fmeum.rules_jni.RulesJni; + +/** + * Loads Android tooling library and registers native functions. + */ +public class AndroidRuntime { + private static final String DO_NOT_INITIALIZE = "use_none"; + private static final String FUZZ_DIR = "/data/fuzz/"; + private static final String PLATFORM_LIB_DIRS = ":/system/lib64/:/apex/com.android.i18n@1/lib64/"; + + public static void initialize(String runtimeLibs) { + if (runtimeLibs == null) { + return; + } + + RulesJni.loadLibrary("jazzer_android_tooling", "/com/code_intelligence/jazzer/driver"); + if (runtimeLibs.equals(DO_NOT_INITIALIZE)) { + Log.warn("Android Runtime (ART) is not being initialized for this fuzzer."); + } else { + registerNatives(); + } + }; + + /** + * Returns a command to set the classpath for fuzzing. + * + * @return The classpath command. + */ + public static String getClassPathsCommand() { + return "export CLASSPATH=" + System.getProperty("java.class.path"); + } + + /** + * Builds and returns the value to set for LD_LIBRARY_PATH. + * This value is consumed when launching jazzer on the device + * and specifies which directories to search for dependencies. + * + * @return The string for LD_LIBRARY_PATH. + */ + public static String getLdLibraryPath() { + String initOptString = System.getProperty("jazzer.android_init_options"); + if (initOptString.equals(DO_NOT_INITIALIZE) || initOptString.equals("")) { + return FUZZ_DIR; + } + + return FUZZ_DIR + PLATFORM_LIB_DIRS; + } + + private static native int registerNatives(); +} diff --git a/src/main/java/com/code_intelligence/jazzer/android/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/android/BUILD.bazel new file mode 100644 index 00000000..1204c4ee --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/android/BUILD.bazel @@ -0,0 +1,95 @@ +load("//bazel:compat.bzl", "SKIP_ON_WINDOWS") +load("@bazel_skylib//rules:copy_file.bzl", "copy_file") +load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library") + +java_import( + name = "jazzer_bootstrap_android_import", + jars = [ + "//src/main/java/com/code_intelligence/jazzer/runtime:jazzer_bootstrap", + ], + tags = ["manual"], + target_compatible_with = SKIP_ON_WINDOWS, +) + +android_library( + name = "jazzer_bootstrap_android_lib", + tags = ["manual"], + target_compatible_with = SKIP_ON_WINDOWS, + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__", + ], + exports = [ + ":jazzer_bootstrap_android_import", + ], +) + +android_binary( + name = "jazzer_bootstrap_android_bin", + manifest = "//launcher/android:android_manifest", + min_sdk_version = 26, + tags = ["manual"], + target_compatible_with = SKIP_ON_WINDOWS, + deps = [ + ":jazzer_bootstrap_android_lib", + ], +) + +copy_file( + name = "jazzer_bootstrap_android", + src = "jazzer_bootstrap_android_bin.apk", + out = "jazzer_bootstrap_android.jar", + tags = ["manual"], + target_compatible_with = SKIP_ON_WINDOWS, + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__", + ], +) + +java_jni_library( + name = "dex_file_manager", + srcs = ["DexFileManager.java"], + native_libs = [ + "//src/main/native/com/code_intelligence/jazzer/android:android_native_agent", + ], +) + +android_library( + name = "jazzer_standalone_library", + tags = ["manual"], + target_compatible_with = SKIP_ON_WINDOWS, + exports = [ + "//deploy:jazzer-api", + "//src/main/java/com/code_intelligence/jazzer:jazzer_import", + ], +) + +android_binary( + name = "jazzer_standalone_android", + manifest = "//launcher/android:android_manifest", + min_sdk_version = 26, + tags = ["manual"], + target_compatible_with = SKIP_ON_WINDOWS, + visibility = [ + "//:__pkg__", + "//launcher/android:__pkg__", + ], + deps = [ + ":dex_file_manager", + ":jazzer_standalone_library", + ], +) + +java_jni_library( + name = "android_runtime", + srcs = ["AndroidRuntime.java"], + native_libs = ["//src/main/native/com/code_intelligence/jazzer/driver:jazzer_android_tooling"], + target_compatible_with = SKIP_ON_WINDOWS, + visibility = [ + "//src/main/java/com/code_intelligence/jazzer:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/driver:__subpackages__", + "//src/main/native/com/code_intelligence/jazzer/driver:__subpackages__", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/utils:log", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/android/DexFileManager.java b/src/main/java/com/code_intelligence/jazzer/android/DexFileManager.java new file mode 100644 index 00000000..23d2eeec --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/android/DexFileManager.java @@ -0,0 +1,68 @@ +/* + * 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.android; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.Math; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class DexFileManager { + private final static int MAX_READ_LENGTH = 2000000; + + public static byte[] getBytecodeFromDex(String jarPath, String dexFile) throws IOException { + try (JarFile jarFile = new JarFile(jarPath)) { + JarEntry entry = jarFile.stream() + .filter(jarEntry -> jarEntry.getName().equals(dexFile)) + .findFirst() + .orElse(null); + + if (entry == null) { + throw new IOException("Could not find dex file: " + dexFile); + } + + try (InputStream is = jarFile.getInputStream(entry)) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + byte[] buffer = new byte[64 * 104 * 1024]; + int read; + while ((read = is.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + + return out.toByteArray(); + } + } + } + + public static String[] getDexFilesForJar(String jarpath) throws IOException { + try (JarFile jarFile = new JarFile(jarpath)) { + return jarFile.stream() + .map(JarEntry::getName) + .filter(entry -> entry.endsWith(".dex")) + .toArray(String[] ::new); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/Autofuzz.java b/src/main/java/com/code_intelligence/jazzer/api/Autofuzz.java new file mode 100644 index 00000000..77711ee9 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/Autofuzz.java @@ -0,0 +1,411 @@ +// Copyright 2021 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.api; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +/** + * Static helper functions that allow Jazzer fuzz targets to use Autofuzz. + */ +final public class Autofuzz { + private static final MethodHandle CONSUME; + private static final MethodHandle AUTOFUZZ_FUNCTION_1; + private static final MethodHandle AUTOFUZZ_FUNCTION_2; + private static final MethodHandle AUTOFUZZ_FUNCTION_3; + private static final MethodHandle AUTOFUZZ_FUNCTION_4; + private static final MethodHandle AUTOFUZZ_FUNCTION_5; + private static final MethodHandle AUTOFUZZ_CONSUMER_1; + private static final MethodHandle AUTOFUZZ_CONSUMER_2; + private static final MethodHandle AUTOFUZZ_CONSUMER_3; + private static final MethodHandle AUTOFUZZ_CONSUMER_4; + private static final MethodHandle AUTOFUZZ_CONSUMER_5; + + static { + MethodHandle consume = null; + MethodHandle autofuzzFunction1 = null; + MethodHandle autofuzzFunction2 = null; + MethodHandle autofuzzFunction3 = null; + MethodHandle autofuzzFunction4 = null; + MethodHandle autofuzzFunction5 = null; + MethodHandle autofuzzConsumer1 = null; + MethodHandle autofuzzConsumer2 = null; + MethodHandle autofuzzConsumer3 = null; + MethodHandle autofuzzConsumer4 = null; + MethodHandle autofuzzConsumer5 = null; + try { + Class<?> metaClass = Class.forName("com.code_intelligence.jazzer.autofuzz.Meta"); + MethodType consumeType = + MethodType.methodType(Object.class, FuzzedDataProvider.class, Class.class); + consume = MethodHandles.publicLookup().findStatic(metaClass, "consume", consumeType); + + autofuzzFunction1 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz", + MethodType.methodType(Object.class, FuzzedDataProvider.class, Function1.class)); + autofuzzFunction2 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz", + MethodType.methodType(Object.class, FuzzedDataProvider.class, Function2.class)); + autofuzzFunction3 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz", + MethodType.methodType(Object.class, FuzzedDataProvider.class, Function3.class)); + autofuzzFunction4 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz", + MethodType.methodType(Object.class, FuzzedDataProvider.class, Function4.class)); + autofuzzFunction5 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz", + MethodType.methodType(Object.class, FuzzedDataProvider.class, Function5.class)); + autofuzzConsumer1 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz", + MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer1.class)); + autofuzzConsumer2 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz", + MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer2.class)); + autofuzzConsumer3 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz", + MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer3.class)); + autofuzzConsumer4 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz", + MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer4.class)); + autofuzzConsumer5 = MethodHandles.publicLookup().findStatic(metaClass, "autofuzz", + MethodType.methodType(void.class, FuzzedDataProvider.class, Consumer5.class)); + } catch (ClassNotFoundException ignore) { + // Not running in the context of the agent. This is fine as long as no methods are called on + // this class. + } catch (NoSuchMethodException | IllegalAccessException e) { + // This should never happen as the Jazzer API is loaded from the agent and thus should always + // match the version of the runtime classes. + // Does not use the Log class as it is unlikely it can be loaded if the Autofuzz classes + // couldn't be loaded. + System.err.println("ERROR: Incompatible version of the Jazzer API detected, please update."); + e.printStackTrace(); + System.exit(1); + } + CONSUME = consume; + AUTOFUZZ_FUNCTION_1 = autofuzzFunction1; + AUTOFUZZ_FUNCTION_2 = autofuzzFunction2; + AUTOFUZZ_FUNCTION_3 = autofuzzFunction3; + AUTOFUZZ_FUNCTION_4 = autofuzzFunction4; + AUTOFUZZ_FUNCTION_5 = autofuzzFunction5; + AUTOFUZZ_CONSUMER_1 = autofuzzConsumer1; + AUTOFUZZ_CONSUMER_2 = autofuzzConsumer2; + AUTOFUZZ_CONSUMER_3 = autofuzzConsumer3; + AUTOFUZZ_CONSUMER_4 = autofuzzConsumer4; + AUTOFUZZ_CONSUMER_5 = autofuzzConsumer5; + } + + private Autofuzz() {} + + /** + * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input + * using only public methods available on the classpath. + * <p> + * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in + * meaningful ways for a number of reasons. + * + * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}. + * @param func a method reference for the function to autofuzz. If there are multiple overloads, + * resolve ambiguities by explicitly casting to {@link Function1} with (partially) specified + * type variables, e.g. {@code (Function1<String, ?>) String::new}. + * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke + * the function. + * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * The {@link Throwable} is thrown unchecked. + */ + @SuppressWarnings("unchecked") + public static <T1, R> R autofuzz(FuzzedDataProvider data, Function1<T1, R> func) { + try { + return (R) AUTOFUZZ_FUNCTION_1.invoke(data, func); + } catch (AutofuzzInvocationException e) { + rethrowUnchecked(e.getCause()); + } catch (Throwable t) { + rethrowUnchecked(t); + } + // Not reached. + return null; + } + + /** + * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input + * using only public methods available on the classpath. + * <p> + * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in + * meaningful ways for a number of reasons. + * + * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}. + * @param func a method reference for the function to autofuzz. If there are multiple overloads, + * resolve ambiguities by explicitly casting to {@link Function2} with (partially) specified + * type variables. + * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke + * the function. + * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * The {@link Throwable} is thrown unchecked. + */ + @SuppressWarnings("unchecked") + public static <T1, T2, R> R autofuzz(FuzzedDataProvider data, Function2<T1, T2, R> func) { + try { + return (R) AUTOFUZZ_FUNCTION_2.invoke(data, func); + } catch (AutofuzzInvocationException e) { + rethrowUnchecked(e.getCause()); + } catch (Throwable t) { + rethrowUnchecked(t); + } + // Not reached. + return null; + } + + /** + * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input + * using only public methods available on the classpath. + * <p> + * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in + * meaningful ways for a number of reasons. + * + * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}. + * @param func a method reference for the function to autofuzz. If there are multiple overloads, + * resolve ambiguities by explicitly casting to {@link Function3} with (partially) specified + * type variables. + * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke + * the function. + * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * The {@link Throwable} is thrown unchecked. + */ + @SuppressWarnings("unchecked") + public static <T1, T2, T3, R> R autofuzz(FuzzedDataProvider data, Function3<T1, T2, T3, R> func) { + try { + return (R) AUTOFUZZ_FUNCTION_3.invoke(data, func); + } catch (AutofuzzInvocationException e) { + rethrowUnchecked(e.getCause()); + } catch (Throwable t) { + rethrowUnchecked(t); + } + // Not reached. + return null; + } + + /** + * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input + * using only public methods available on the classpath. + * <p> + * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in + * meaningful ways for a number of reasons. + * + * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}. + * @param func a method reference for the function to autofuzz. If there are multiple overloads, + * resolve ambiguities by explicitly casting to {@link Function4} with (partially) specified + * type variables. + * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke + * the function. + * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * The {@link Throwable} is thrown unchecked. + */ + @SuppressWarnings("unchecked") + public static <T1, T2, T3, T4, R> R autofuzz( + FuzzedDataProvider data, Function4<T1, T2, T3, T4, R> func) { + try { + return (R) AUTOFUZZ_FUNCTION_4.invoke(data, func); + } catch (AutofuzzInvocationException e) { + rethrowUnchecked(e.getCause()); + } catch (Throwable t) { + rethrowUnchecked(t); + } + // Not reached. + return null; + } + + /** + * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input + * using only public methods available on the classpath. + * <p> + * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in + * meaningful ways for a number of reasons. + * + * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}. + * @param func a method reference for the function to autofuzz. If there are multiple overloads, + * resolve ambiguities by explicitly casting to {@link Function5} with (partially) specified + * type variables. + * @return the return value of {@code func}, or {@code null} if {@code autofuzz} failed to invoke + * the function. + * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * The {@link Throwable} is thrown unchecked. + */ + @SuppressWarnings("unchecked") + public static <T1, T2, T3, T4, T5, R> R autofuzz( + FuzzedDataProvider data, Function5<T1, T2, T3, T4, T5, R> func) { + try { + return (R) AUTOFUZZ_FUNCTION_5.invoke(data, func); + } catch (AutofuzzInvocationException e) { + rethrowUnchecked(e.getCause()); + } catch (Throwable t) { + rethrowUnchecked(t); + } + // Not reached. + return null; + } + + /** + * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input + * using only public methods available on the classpath. + * <p> + * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in + * meaningful ways for a number of reasons. + * + * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}. + * @param func a method reference for the function to autofuzz. If there are multiple overloads, + * resolve ambiguities by explicitly casting to {@link Consumer1} with explicitly specified + * type variable. + * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * The {@link Throwable} is thrown unchecked. + */ + public static <T1> void autofuzz(FuzzedDataProvider data, Consumer1<T1> func) { + try { + AUTOFUZZ_CONSUMER_1.invoke(data, func); + } catch (AutofuzzInvocationException e) { + rethrowUnchecked(e.getCause()); + } catch (Throwable t) { + rethrowUnchecked(t); + } + } + + /** + * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input + * using only public methods available on the classpath. + * <p> + * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in + * meaningful ways for a number of reasons. + * + * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}. + * @param func a method reference for the function to autofuzz. If there are multiple overloads, + * resolve ambiguities by explicitly casting to {@link Consumer2} with (partially) specified + * type variables. + * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * The {@link Throwable} is thrown unchecked. + */ + public static <T1, T2> void autofuzz(FuzzedDataProvider data, Consumer2<T1, T2> func) { + try { + AUTOFUZZ_CONSUMER_2.invoke(data, func); + } catch (AutofuzzInvocationException e) { + rethrowUnchecked(e.getCause()); + } catch (Throwable t) { + rethrowUnchecked(t); + } + } + + /** + * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input + * using only public methods available on the classpath. + * <p> + * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in + * meaningful ways for a number of reasons. + * + * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}. + * @param func a method reference for the function to autofuzz. If there are multiple overloads, + * resolve ambiguities by explicitly casting to {@link Consumer3} with (partially) specified + * type variables. + * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * The {@link Throwable} is thrown unchecked. + */ + public static <T1, T2, T3> void autofuzz(FuzzedDataProvider data, Consumer3<T1, T2, T3> func) { + try { + AUTOFUZZ_CONSUMER_3.invoke(data, func); + } catch (AutofuzzInvocationException e) { + rethrowUnchecked(e.getCause()); + } catch (Throwable t) { + rethrowUnchecked(t); + } + } + + /** + * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input + * using only public methods available on the classpath. + * <p> + * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in + * meaningful ways for a number of reasons. + * + * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}. + * @param func a method reference for the function to autofuzz. If there are multiple overloads, + * resolve ambiguities by explicitly casting to {@link Consumer4} with (partially) specified + * type variables. + * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * The {@link Throwable} is thrown unchecked. + */ + public static <T1, T2, T3, T4> void autofuzz( + FuzzedDataProvider data, Consumer4<T1, T2, T3, T4> func) { + try { + AUTOFUZZ_CONSUMER_4.invoke(data, func); + } catch (AutofuzzInvocationException e) { + rethrowUnchecked(e.getCause()); + } catch (Throwable t) { + rethrowUnchecked(t); + } + } + + /** + * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input + * using only public methods available on the classpath. + * <p> + * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in + * meaningful ways for a number of reasons. + * + * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}. + * @param func a method reference for the function to autofuzz. If there are multiple overloads, + * resolve ambiguities by explicitly casting to {@link Consumer5} with (partially) specified + * type variables. + * @throws Throwable any {@link Throwable} thrown by {@code func}, or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * The {@link Throwable} is thrown unchecked. + */ + public static <T1, T2, T3, T4, T5> void autofuzz( + FuzzedDataProvider data, Consumer5<T1, T2, T3, T4, T5> func) { + try { + AUTOFUZZ_CONSUMER_5.invoke(data, func); + } catch (AutofuzzInvocationException e) { + rethrowUnchecked(e.getCause()); + } catch (Throwable t) { + rethrowUnchecked(t); + } + } + + /** + * Attempts to construct an instance of {@code type} from the fuzzer input using only public + * methods available on the classpath. + * <p> + * <b>Note:</b> This function is inherently heuristic and may fail to return meaningful values for + * a variety of reasons. + * + * @param data the {@link FuzzedDataProvider} instance provided to {@code fuzzerTestOneInput}. + * @param type the {@link Class} to construct an instance of. + * @return an instance of {@code type} constructed from the fuzzer input, or {@code null} if + * autofuzz failed to create an instance. + */ + @SuppressWarnings("unchecked") + public static <T> T consume(FuzzedDataProvider data, Class<T> type) { + try { + return (T) CONSUME.invokeExact(data, type); + } catch (AutofuzzConstructionException ignored) { + return null; + } catch (Throwable t) { + rethrowUnchecked(t); + // Not reached. + return null; + } + } + + // Rethrows a (possibly checked) exception while avoiding a throws declaration. + @SuppressWarnings("unchecked") + private static <T extends Throwable> void rethrowUnchecked(Throwable t) throws T { + throw(T) t; + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/AutofuzzConstructionException.java b/src/main/java/com/code_intelligence/jazzer/api/AutofuzzConstructionException.java new file mode 100644 index 00000000..93340ee8 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/AutofuzzConstructionException.java @@ -0,0 +1,32 @@ +// Copyright 2021 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.api; + +// An exception wrapping a Throwable thrown during the construction of parameters for, but not the +// actual invocation of an autofuzzed method. +/** + * Only used internally. + */ +public class AutofuzzConstructionException extends RuntimeException { + public AutofuzzConstructionException() { + super(); + } + public AutofuzzConstructionException(String message) { + super(message); + } + public AutofuzzConstructionException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/AutofuzzInvocationException.java b/src/main/java/com/code_intelligence/jazzer/api/AutofuzzInvocationException.java new file mode 100644 index 00000000..eb936630 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/AutofuzzInvocationException.java @@ -0,0 +1,30 @@ +// Copyright 2021 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.api; + +// An exception wrapping a {@link Throwable} thrown during the actual invocation of, but not the +// construction of parameters for an autofuzzed method. +/** + * Only used internally. + */ +public class AutofuzzInvocationException extends RuntimeException { + public AutofuzzInvocationException() { + super(); + } + + public AutofuzzInvocationException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel new file mode 100644 index 00000000..03ac7917 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel @@ -0,0 +1,51 @@ +load("@rules_jvm_external//:defs.bzl", "java_export") + +java_library( + name = "api", + srcs = [ + "Autofuzz.java", + "AutofuzzConstructionException.java", + "AutofuzzInvocationException.java", + "BugDetectors.java", + "CannedFuzzedDataProvider.java", + "Consumer1.java", + "Consumer2.java", + "Consumer3.java", + "Consumer4.java", + "Consumer5.java", + "Function1.java", + "Function2.java", + "Function3.java", + "Function4.java", + "Function5.java", + "FuzzedDataProvider.java", + "SilentCloseable.java", + ], + visibility = ["//visibility:public"], + runtime_deps = [ + ":hooks", + ], +) + +java_binary( + name = "api_deploy_env", + create_executable = False, + visibility = ["//src/main/java/com/code_intelligence/jazzer:__pkg__"], + runtime_deps = [":api"], +) + +java_library( + name = "hooks", + srcs = [ + "FuzzerSecurityIssueCritical.java", + "FuzzerSecurityIssueHigh.java", + "FuzzerSecurityIssueLow.java", + "FuzzerSecurityIssueMedium.java", + "HookType.java", + "Jazzer.java", + "MethodHook.java", + "MethodHooks.java", + "//src/main/java/jaz", + ], + visibility = ["//visibility:public"], +) diff --git a/src/main/java/com/code_intelligence/jazzer/api/BugDetectors.java b/src/main/java/com/code_intelligence/jazzer/api/BugDetectors.java new file mode 100644 index 00000000..64f01931 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/BugDetectors.java @@ -0,0 +1,111 @@ +/* + * 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.api; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiPredicate; + +/** + * Provides static functions that configure the behavior of bug detectors provided by Jazzer. + */ +public final class BugDetectors { + private static final AtomicReference<BiPredicate<String, Integer>> currentPolicy = + getConnectionPermittedReference(); + + /** + * Allows all network connections. + * + * <p>See {@link #allowNetworkConnections(BiPredicate)} for an alternative that provides + * fine-grained control over which network connections are expected. + * + * <p>By default, all attempted network connections are considered unexpected and result in a + * finding being reported. + * + * <p>By wrapping the call into a try-with-resources statement, network connection permissions + * can be configured to apply to individual parts of the fuzz test only: + * + * <pre>{@code + * Image image = parseImage(bytes); + * Response response; + * try (SilentCloseable unused = BugDetectors.allowNetworkConnections()) { + * response = uploadImage(image); + * } + * handleResponse(response); + * }</pre> + * + * @return a {@link SilentCloseable} that restores the previously set permissions when closed + */ + public static SilentCloseable allowNetworkConnections() { + return allowNetworkConnections((host, port) -> true); + } + + /** + * Allows all network connections for which the provided predicate returns {@code true}. + * + * <p>By default, all attempted network connections are considered unexpected and result in a + * finding being reported. + * + * <p>By wrapping the call into a try-with-resources statement, network connection permissions + * can be configured to apply to individual parts of the fuzz test only: + * + * <pre>{@code + * Image image = parseImage(bytes); + * Response response; + * try (SilentCloseable unused = BugDetectors.allowNetworkConnections( + * (host, port) -> host.equals("example.org"))) { + * response = uploadImage(image, "example.org"); + * } + * handleResponse(response); + * }</pre> + * + * @param connectionPermitted a predicate that evaluate to {@code true} if network connections to + * the provided combination of host and port are permitted + * @return a {@link SilentCloseable} that restores the previously set predicate when closed + */ + public static SilentCloseable allowNetworkConnections( + BiPredicate<String, Integer> connectionPermitted) { + if (connectionPermitted == null) { + throw new IllegalArgumentException("connectionPermitted must not be null"); + } + if (currentPolicy == null) { + throw new IllegalStateException("Failed to set network connection policy"); + } + BiPredicate<String, Integer> previousPolicy = currentPolicy.getAndSet(connectionPermitted); + return () -> { + if (!currentPolicy.compareAndSet(connectionPermitted, previousPolicy)) { + throw new IllegalStateException( + "Failed to reset network connection policy - using try-with-resources is highly recommended"); + } + }; + } + + private static AtomicReference<BiPredicate<String, Integer>> getConnectionPermittedReference() { + try { + Class<?> ssrfSanitizer = + Class.forName("com.code_intelligence.jazzer.sanitizers.ServerSideRequestForgery"); + return (AtomicReference<BiPredicate<String, Integer>>) ssrfSanitizer + .getField("connectionPermitted") + .get(null); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + System.err.println("WARNING: "); + e.printStackTrace(); + return null; + } + } + + private BugDetectors() {} +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/CannedFuzzedDataProvider.java b/src/main/java/com/code_intelligence/jazzer/api/CannedFuzzedDataProvider.java new file mode 100644 index 00000000..7209a497 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/CannedFuzzedDataProvider.java @@ -0,0 +1,211 @@ +// Copyright 2021 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.api; + +import java.io.*; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Iterator; +import java.util.List; + +/** + * Replays recorded FuzzedDataProvider invocations that were executed while fuzzing. + * Note: This class is only meant to be used by Jazzer's generated reproducers. + */ +final public class CannedFuzzedDataProvider implements FuzzedDataProvider { + private final Iterator<Object> nextReply; + + public CannedFuzzedDataProvider(String can) { + byte[] rawIn = Base64.getDecoder().decode(can); + ArrayList<Object> recordedReplies; + try (ByteArrayInputStream byteStream = new ByteArrayInputStream(rawIn)) { + try (ObjectInputStream objectStream = new ObjectInputStream(byteStream)) { + recordedReplies = (ArrayList<Object>) objectStream.readObject(); + } + } catch (IOException | ClassNotFoundException e) { + throw new RuntimeException(e); + } + nextReply = recordedReplies.iterator(); + } + + public static CannedFuzzedDataProvider create(List<Object> objects) { + try { + try (ByteArrayOutputStream bout = new ByteArrayOutputStream()) { + try (ObjectOutputStream out = new ObjectOutputStream(bout)) { + out.writeObject(new ArrayList<>(objects)); + String base64 = Base64.getEncoder().encodeToString(bout.toByteArray()); + return new CannedFuzzedDataProvider(base64); + } + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @Override + public boolean consumeBoolean() { + return (boolean) nextReply.next(); + } + + @Override + public boolean[] consumeBooleans(int maxLength) { + return (boolean[]) nextReply.next(); + } + + @Override + public byte consumeByte() { + return (byte) nextReply.next(); + } + + @Override + public byte consumeByte(byte min, byte max) { + return (byte) nextReply.next(); + } + + @Override + public short consumeShort() { + return (short) nextReply.next(); + } + + @Override + public short consumeShort(short min, short max) { + return (short) nextReply.next(); + } + + @Override + public short[] consumeShorts(int maxLength) { + return (short[]) nextReply.next(); + } + + @Override + public int consumeInt() { + return (int) nextReply.next(); + } + + @Override + public int consumeInt(int min, int max) { + return (int) nextReply.next(); + } + + @Override + public int[] consumeInts(int maxLength) { + return (int[]) nextReply.next(); + } + + @Override + public long consumeLong() { + return (long) nextReply.next(); + } + + @Override + public long consumeLong(long min, long max) { + return (long) nextReply.next(); + } + + @Override + public long[] consumeLongs(int maxLength) { + return (long[]) nextReply.next(); + } + + @Override + public float consumeFloat() { + return (float) nextReply.next(); + } + + @Override + public float consumeRegularFloat() { + return (float) nextReply.next(); + } + + @Override + public float consumeRegularFloat(float min, float max) { + return (float) nextReply.next(); + } + + @Override + public float consumeProbabilityFloat() { + return (float) nextReply.next(); + } + + @Override + public double consumeDouble() { + return (double) nextReply.next(); + } + + @Override + public double consumeRegularDouble(double min, double max) { + return (double) nextReply.next(); + } + + @Override + public double consumeRegularDouble() { + return (double) nextReply.next(); + } + + @Override + public double consumeProbabilityDouble() { + return (double) nextReply.next(); + } + + @Override + public char consumeChar() { + return (char) nextReply.next(); + } + + @Override + public char consumeChar(char min, char max) { + return (char) nextReply.next(); + } + + @Override + public char consumeCharNoSurrogates() { + return (char) nextReply.next(); + } + + @Override + public String consumeAsciiString(int maxLength) { + return (String) nextReply.next(); + } + + @Override + public String consumeString(int maxLength) { + return (String) nextReply.next(); + } + + @Override + public String consumeRemainingAsAsciiString() { + return (String) nextReply.next(); + } + + @Override + public String consumeRemainingAsString() { + return (String) nextReply.next(); + } + + @Override + public byte[] consumeBytes(int maxLength) { + return (byte[]) nextReply.next(); + } + + @Override + public byte[] consumeRemainingAsBytes() { + return (byte[]) nextReply.next(); + } + + @Override + public int remainingBytes() { + return (int) nextReply.next(); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/Consumer1.java b/src/main/java/com/code_intelligence/jazzer/api/Consumer1.java new file mode 100644 index 00000000..472c2efd --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/Consumer1.java @@ -0,0 +1,22 @@ +// Copyright 2021 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.api; + +import java.util.function.Consumer; + +@FunctionalInterface +public interface Consumer1<T1> extends Consumer<T1> { + @Override void accept(T1 t1); +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/Consumer2.java b/src/main/java/com/code_intelligence/jazzer/api/Consumer2.java new file mode 100644 index 00000000..d951ade7 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/Consumer2.java @@ -0,0 +1,22 @@ +// Copyright 2021 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.api; + +import java.util.function.BiConsumer; + +@FunctionalInterface +public interface Consumer2<T1, T2> extends BiConsumer<T1, T2> { + @Override void accept(T1 t1, T2 t2); +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/Consumer3.java b/src/main/java/com/code_intelligence/jazzer/api/Consumer3.java new file mode 100644 index 00000000..c508fe53 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/Consumer3.java @@ -0,0 +1,20 @@ +// Copyright 2021 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.api; + +@FunctionalInterface +public interface Consumer3<T1, T2, T3> { + void accept(T1 t1, T2 t2, T3 t3); +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/Consumer4.java b/src/main/java/com/code_intelligence/jazzer/api/Consumer4.java new file mode 100644 index 00000000..6ee70141 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/Consumer4.java @@ -0,0 +1,20 @@ +// Copyright 2021 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.api; + +@FunctionalInterface +public interface Consumer4<T1, T2, T3, T4> { + void accept(T1 t1, T2 t2, T3 t3, T4 t4); +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/Consumer5.java b/src/main/java/com/code_intelligence/jazzer/api/Consumer5.java new file mode 100644 index 00000000..523df53c --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/Consumer5.java @@ -0,0 +1,20 @@ +// Copyright 2021 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.api; + +@FunctionalInterface +public interface Consumer5<T1, T2, T3, T4, T5> { + void accept(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5); +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/Function1.java b/src/main/java/com/code_intelligence/jazzer/api/Function1.java new file mode 100644 index 00000000..43d68cc7 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/Function1.java @@ -0,0 +1,22 @@ +// Copyright 2021 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.api; + +import java.util.function.Function; + +@FunctionalInterface +public interface Function1<T1, R> extends Function<T1, R> { + @Override R apply(T1 t1); +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/Function2.java b/src/main/java/com/code_intelligence/jazzer/api/Function2.java new file mode 100644 index 00000000..6e733b1c --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/Function2.java @@ -0,0 +1,22 @@ +// Copyright 2021 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.api; + +import java.util.function.BiFunction; + +@FunctionalInterface +public interface Function2<T1, T2, R> extends BiFunction<T1, T2, R> { + @Override R apply(T1 t1, T2 t2); +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/Function3.java b/src/main/java/com/code_intelligence/jazzer/api/Function3.java new file mode 100644 index 00000000..07d593f9 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/Function3.java @@ -0,0 +1,20 @@ +// Copyright 2021 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.api; + +@FunctionalInterface +public interface Function3<T1, T2, T3, R> { + R apply(T1 t1, T2 t2, T3 t3); +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/Function4.java b/src/main/java/com/code_intelligence/jazzer/api/Function4.java new file mode 100644 index 00000000..0e6ec75e --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/Function4.java @@ -0,0 +1,20 @@ +// Copyright 2021 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.api; + +@FunctionalInterface +public interface Function4<T1, T2, T3, T4, R> { + R apply(T1 t1, T2 t2, T3 t3, T4 t4); +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/Function5.java b/src/main/java/com/code_intelligence/jazzer/api/Function5.java new file mode 100644 index 00000000..cd833f78 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/Function5.java @@ -0,0 +1,20 @@ +// Copyright 2021 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.api; + +@FunctionalInterface +public interface Function5<T1, T2, T3, T4, T5, R> { + R apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5); +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/FuzzedDataProvider.java b/src/main/java/com/code_intelligence/jazzer/api/FuzzedDataProvider.java new file mode 100644 index 00000000..b1f38b50 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/FuzzedDataProvider.java @@ -0,0 +1,444 @@ +// Copyright 2021 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.api; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Random; + +/** + * A convenience wrapper turning the raw fuzzer input bytes into Java primitive types. + * + * <p>The methods defined by this interface behave similarly to {@link Random#nextInt()}, with all + * returned values depending deterministically on the fuzzer input for the current run. + */ +public interface FuzzedDataProvider { + /** + * Consumes a {@code boolean} from the fuzzer input. + * + * @return a {@code boolean} + */ + boolean consumeBoolean(); + + /** + * Consumes a {@code boolean} array from the fuzzer input. + * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input + * is not sufficiently long. + * + * @param maxLength the maximum length of the array + * @return a {@code boolean} array of length at most {@code length} + */ + boolean[] consumeBooleans(int maxLength); + + /** + * Consumes a {@code byte} from the fuzzer input. + * + * @return a {@code byte} + */ + byte consumeByte(); + + /** + * Consumes a {@code byte} between {@code min} and {@code max} from the fuzzer input. + * + * @param min the inclusive lower bound on the returned value + * @param max the inclusive upper bound on the returned value + * @return a {@code byte} in the range {@code [min, max]} + */ + byte consumeByte(byte min, byte max); + + /** + * Consumes a {@code byte} array from the fuzzer input. + * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input + * is not sufficiently long. + * + * @param maxLength the maximum length of the array + * @return a {@code byte} array of length at most {@code length} + */ + byte[] consumeBytes(int maxLength); + + /** + * Consumes the remaining fuzzer input as a {@code byte} array. + * <p><b>Note:</b> After calling this method, further calls to methods of this interface will + * return fixed values only. + * + * @return a {@code byte} array + */ + byte[] consumeRemainingAsBytes(); + + /** + * Consumes a {@code short} from the fuzzer input. + * + * @return a {@code short} + */ + short consumeShort(); + + /** + * Consumes a {@code short} between {@code min} and {@code max} from the fuzzer input. + * + * @param min the inclusive lower bound on the returned value + * @param max the inclusive upper bound on the returned value + * @return a {@code short} in the range {@code [min, max]} + */ + short consumeShort(short min, short max); + + /** + * Consumes a {@code short} array from the fuzzer input. + * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input + * is not sufficiently long. + * + * @param maxLength the maximum length of the array + * @return a {@code short} array of length at most {@code length} + */ + short[] consumeShorts(int maxLength); + + /** + * Consumes an {@code int} from the fuzzer input. + * + * @return an {@code int} + */ + int consumeInt(); + + /** + * Consumes an {@code int} between {@code min} and {@code max} from the fuzzer input. + * + * @param min the inclusive lower bound on the returned value + * @param max the inclusive upper bound on the returned value + * @return an {@code int} in the range {@code [min, max]} + */ + int consumeInt(int min, int max); + + /** + * Consumes an {@code int} array from the fuzzer input. + * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input + * is not sufficiently long. + * + * @param maxLength the maximum length of the array + * @return an {@code int} array of length at most {@code length} + */ + int[] consumeInts(int maxLength); + + /** + * Consumes a {@code long} from the fuzzer input. + * + * @return a {@code long} + */ + long consumeLong(); + + /** + * Consumes a {@code long} between {@code min} and {@code max} from the fuzzer input. + * + * @param min the inclusive lower bound on the returned value + * @param max the inclusive upper bound on the returned value + * @return a {@code long} in the range @{code [min, max]} + */ + long consumeLong(long min, long max); + + /** + * Consumes a {@code long} array from the fuzzer input. + * <p>The array will usually have length {@code length}, but might be shorter if the fuzzer input + * is not sufficiently long. + * + * @param maxLength the maximum length of the array + * @return a {@code long} array of length at most {@code length} + */ + long[] consumeLongs(int maxLength); + + /** + * Consumes a {@code float} from the fuzzer input. + * + * @return a {@code float} that may have a special value (e.g. a NaN or infinity) + */ + float consumeFloat(); + + /** + * Consumes a regular {@code float} from the fuzzer input. + * + * @return a {@code float} that is not a special value (e.g. not a NaN or infinity) + */ + float consumeRegularFloat(); + + /** + * Consumes a regular {@code float} between {@code min} and {@code max} from the fuzzer input. + * + * @return a {@code float} in the range {@code [min, max]} + */ + float consumeRegularFloat(float min, float max); + + /** + * Consumes a {@code float} between 0.0 and 1.0 (inclusive) from the fuzzer input. + * + * @return a {@code float} in the range {@code [0.0, 1.0]} + */ + float consumeProbabilityFloat(); + + /** + * Consumes a {@code double} from the fuzzer input. + * + * @return a {@code double} that may have a special value (e.g. a NaN or infinity) + */ + double consumeDouble(); + + /** + * Consumes a regular {@code double} from the fuzzer input. + * + * @return a {@code double} that is not a special value (e.g. not a NaN or infinity) + */ + double consumeRegularDouble(); + + /** + * Consumes a regular {@code double} between {@code min} and {@code max} from the fuzzer input. + * + * @return a {@code double} in the range {@code [min, max]} + */ + double consumeRegularDouble(double min, double max); + + /** + * Consumes a {@code double} between 0.0 and 1.0 (inclusive) from the fuzzer input. + * + * @return a {@code double} in the range {@code [0.0, 1.0]} + */ + double consumeProbabilityDouble(); + + /** + * Consumes a {@code char} from the fuzzer input. + */ + char consumeChar(); + + /** + * Consumes a {@code char} between {@code min} and {@code max} from the fuzzer input. + * + * @param min the inclusive lower bound on the returned value + * @param max the inclusive upper bound on the returned value + * @return a {@code char} in the range {@code [min, max]} + */ + char consumeChar(char min, char max); + + /** + * Consumes a {@code char} from the fuzzer input that is never a UTF-16 surrogate character. + */ + char consumeCharNoSurrogates(); + + /** + * Consumes a {@link String} from the fuzzer input. + * <p>The returned string may be of any length between 0 and {@code maxLength}, even if there is + * more fuzzer input available. + * + * @param maxLength the maximum length of the string + * @return a {@link String} of length between 0 and {@code maxLength} (inclusive) + */ + String consumeString(int maxLength); + + /** + * Consumes the remaining fuzzer input as a {@link String}. + * <p><b>Note:</b> After calling this method, further calls to methods of this interface will + * return fixed values only. + * + * @return a {@link String} + */ + String consumeRemainingAsString(); + + /** + * Consumes an ASCII-only {@link String} from the fuzzer input. + * <p>The returned string may be of any length between 0 and {@code maxLength}, even if there is + * more fuzzer input available. + * + * @param maxLength the maximum length of the string + * @return a {@link String} of length between 0 and {@code maxLength} (inclusive) that contains + * only ASCII characters + */ + String consumeAsciiString(int maxLength); + + /** + * Consumes the remaining fuzzer input as an ASCII-only {@link String}. + * <p><b>Note:</b> After calling this method, further calls to methods of this interface will + * return fixed values only. + * + * @return a {@link String} that contains only ASCII characters + */ + String consumeRemainingAsAsciiString(); + + /** + * Returns the number of unconsumed bytes in the fuzzer input. + * + * @return the number of unconsumed bytes in the fuzzer input + */ + int remainingBytes(); + + /** + * Picks an element from {@code collection} based on the fuzzer input. + * <p><b>Note:</b> The distribution of picks is not perfectly uniform. + * + * @param collection the {@link Collection} to pick an element from. + * @param <T> the type of the collection element + * @return an element from {@code collection} chosen based on the fuzzer input + */ + @SuppressWarnings("unchecked") + default<T> T pickValue(Collection<T> collection) { + int size = collection.size(); + if (size == 0) { + throw new IllegalArgumentException("collection is empty"); + } + if (collection instanceof List<?>) { + return ((List<T>) collection).get(consumeInt(0, size - 1)); + } else { + return (T) pickValue(collection.toArray()); + } + } + + /** + * Picks an element from {@code array} based on the fuzzer input. + * <p><b>Note:</b> The distribution of picks is not perfectly uniform. + * + * @param array the array to pick an element from. + * @param <T> the type of the array element + * @return an element from {@code array} chosen based on the fuzzer input + */ + default<T> T pickValue(T[] array) { + return array[consumeInt(0, array.length - 1)]; + } + + /** + * Picks an element from {@code array} based on the fuzzer input. + * <p><b>Note:</b> The distribution of picks is not perfectly uniform. + * + * @param array the array to pick an element from. + * @return an element from {@code array} chosen based on the fuzzer input + */ + default boolean pickValue(boolean[] array) { + return array[consumeInt(0, array.length - 1)]; + } + + /** + * Picks an element from {@code array} based on the fuzzer input. + * <p><b>Note:</b> The distribution of picks is not perfectly uniform. + * + * @param array the array to pick an element from. + * @return an element from {@code array} chosen based on the fuzzer input + */ + default byte pickValue(byte[] array) { + return array[consumeInt(0, array.length - 1)]; + } + + /** + * Picks an element from {@code array} based on the fuzzer input. + * <p><b>Note:</b> The distribution of picks is not perfectly uniform. + * + * @param array the array to pick an element from. + * @return an element from {@code array} chosen based on the fuzzer input + */ + default short pickValue(short[] array) { + return array[consumeInt(0, array.length - 1)]; + } + + /** + * Picks an element from {@code array} based on the fuzzer input. + * <p><b>Note:</b> The distribution of picks is not perfectly uniform. + * + * @param array the array to pick an element from. + * @return an element from {@code array} chosen based on the fuzzer input + */ + default int pickValue(int[] array) { + return array[consumeInt(0, array.length - 1)]; + } + + /** + * Picks an element from {@code array} based on the fuzzer input. + * <p><b>Note:</b> The distribution of picks is not perfectly uniform. + * + * @param array the array to pick an element from. + * @return an element from {@code array} chosen based on the fuzzer input + */ + default long pickValue(long[] array) { + return array[consumeInt(0, array.length - 1)]; + } + + /** + * Picks an element from {@code array} based on the fuzzer input. + * <p><b>Note:</b> The distribution of picks is not perfectly uniform. + * + * @param array the array to pick an element from. + * @return an element from {@code array} chosen based on the fuzzer input + */ + default double pickValue(double[] array) { + return array[consumeInt(0, array.length - 1)]; + } + + /** + * Picks an element from {@code array} based on the fuzzer input. + * <p><b>Note:</b> The distribution of picks is not perfectly uniform. + * + * @param array the array to pick an element from. + * @return an element from {@code array} chosen based on the fuzzer input + */ + default float pickValue(float[] array) { + return array[consumeInt(0, array.length - 1)]; + } + + /** + * Picks an element from {@code array} based on the fuzzer input. + * <p><b>Note:</b> The distribution of picks is not perfectly uniform. + * + * @param array the array to pick an element from. + * @return an element from {@code array} chosen based on the fuzzer input + */ + default char pickValue(char[] array) { + return array[consumeInt(0, array.length - 1)]; + } + + /** + * Picks {@code numOfElements} elements from {@code collection} based on the fuzzer input. + * <p><b>Note:</b> The distribution of picks is not perfectly uniform. + * + * @param collection the {@link Collection} to pick an element from. + * @param numOfElements the number of elements to pick. + * @param <T> the type of the collection element + * @return an array of size {@code numOfElements} from {@code collection} chosen based on the + * fuzzer input + */ + default<T> List<T> pickValues(Collection<T> collection, int numOfElements) { + int size = collection.size(); + if (size == 0) { + throw new IllegalArgumentException("collection is empty"); + } + if (numOfElements > collection.size()) { + throw new IllegalArgumentException("numOfElements exceeds collection.size()"); + } + + List<T> remainingElements = new ArrayList<>(collection); + List<T> pickedElements = new ArrayList<>(); + for (int i = 0; i < numOfElements; i++) { + T element = pickValue(remainingElements); + pickedElements.add(element); + remainingElements.remove(element); + } + return pickedElements; + } + + /** + * Picks {@code numOfElements} elements from {@code array} based on the fuzzer input. + * <p><b>Note:</b> The distribution of picks is not perfectly uniform. + * + * @param array the array to pick an element from. + * @param numOfElements the number of elements to pick. + * @param <T> the type of the array element + * @return an array of size {@code numOfElements} from {@code array} chosen based on the fuzzer + * input + */ + default<T> List<T> pickValues(T[] array, int numOfElements) { + return pickValues(Arrays.asList(array), numOfElements); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java new file mode 100644 index 00000000..fbde853b --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java @@ -0,0 +1,39 @@ +// Copyright 2021 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.api; + +/** + * Thrown to indicate that a fuzz target has detected a critical severity security issue rather than + * a normal bug. + * <p> + * There is only a semantical but no functional difference between throwing exceptions of this type + * or any other. However, automated fuzzing platforms can use the extra information to handle the + * detected issues appropriately. + */ +public class FuzzerSecurityIssueCritical extends RuntimeException { + public FuzzerSecurityIssueCritical() {} + + public FuzzerSecurityIssueCritical(String message) { + super(message); + } + + public FuzzerSecurityIssueCritical(String message, Throwable cause) { + super(message, cause); + } + + public FuzzerSecurityIssueCritical(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java new file mode 100644 index 00000000..05837b0e --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java @@ -0,0 +1,39 @@ +// Copyright 2021 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.api; + +/** + * Thrown to indicate that a fuzz target has detected a high severity security issue rather than a + * normal bug. + * <p> + * There is only a semantical but no functional difference between throwing exceptions of this type + * or any other. However, automated fuzzing platforms can use the extra information to handle the + * detected issues appropriately. + */ +public class FuzzerSecurityIssueHigh extends RuntimeException { + public FuzzerSecurityIssueHigh() {} + + public FuzzerSecurityIssueHigh(String message) { + super(message); + } + + public FuzzerSecurityIssueHigh(String message, Throwable cause) { + super(message, cause); + } + + public FuzzerSecurityIssueHigh(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueLow.java b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueLow.java new file mode 100644 index 00000000..364b3afb --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueLow.java @@ -0,0 +1,39 @@ +// Copyright 2021 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.api; + +/** + * Thrown to indicate that a fuzz target has detected a low severity security issue rather than a + * normal bug. + * + * There is only a semantical but no functional difference between throwing exceptions of this type + * or any other. However, automated fuzzing platforms can use the extra information to handle the + * detected issues appropriately. + */ +public class FuzzerSecurityIssueLow extends RuntimeException { + public FuzzerSecurityIssueLow() {} + + public FuzzerSecurityIssueLow(String message) { + super(message); + } + + public FuzzerSecurityIssueLow(String message, Throwable cause) { + super(message, cause); + } + + public FuzzerSecurityIssueLow(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java new file mode 100644 index 00000000..be7c8c8f --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java @@ -0,0 +1,39 @@ +// Copyright 2021 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.api; + +/** + * Thrown to indicate that a fuzz target has detected a medium severity security issue rather than a + * normal bug. + * <p> + * There is only a semantical but no functional difference between throwing exceptions of this type + * or any other. However, automated fuzzing platforms can use the extra information to handle the + * detected issues appropriately. + */ +public class FuzzerSecurityIssueMedium extends RuntimeException { + public FuzzerSecurityIssueMedium() {} + + public FuzzerSecurityIssueMedium(String message) { + super(message); + } + + public FuzzerSecurityIssueMedium(String message, Throwable cause) { + super(message, cause); + } + + public FuzzerSecurityIssueMedium(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/HookType.java b/src/main/java/com/code_intelligence/jazzer/api/HookType.java new file mode 100644 index 00000000..8ed4337f --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/HookType.java @@ -0,0 +1,25 @@ +// Copyright 2021 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.api; + +/** + * The type of a {@link MethodHook}. + */ +// Note: The order of entries is important and is used during instrumentation. +public enum HookType { + BEFORE, + REPLACE, + AFTER, +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java b/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java new file mode 100644 index 00000000..aad9ae01 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java @@ -0,0 +1,268 @@ +// Copyright 2021 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.api; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.InvocationTargetException; +import java.security.SecureRandom; + +/** + * Static helper methods that hooks can use to provide feedback to the fuzzer. + */ +public final class Jazzer { + private static final Class<?> JAZZER_INTERNAL; + + private static final MethodHandle ON_FUZZ_TARGET_READY; + + private static final MethodHandle TRACE_STRCMP; + private static final MethodHandle TRACE_STRSTR; + private static final MethodHandle TRACE_MEMCMP; + private static final MethodHandle TRACE_PC_INDIR; + + static { + Class<?> jazzerInternal = null; + MethodHandle onFuzzTargetReady = null; + MethodHandle traceStrcmp = null; + MethodHandle traceStrstr = null; + MethodHandle traceMemcmp = null; + MethodHandle tracePcIndir = null; + try { + jazzerInternal = Class.forName("com.code_intelligence.jazzer.runtime.JazzerInternal"); + MethodType onFuzzTargetReadyType = MethodType.methodType(void.class, Runnable.class); + onFuzzTargetReady = MethodHandles.publicLookup().findStatic( + jazzerInternal, "registerOnFuzzTargetReadyCallback", onFuzzTargetReadyType); + Class<?> traceDataFlowNativeCallbacks = + Class.forName("com.code_intelligence.jazzer.runtime.TraceDataFlowNativeCallbacks"); + + // Use method handles for hints as the calls are potentially performance critical. + MethodType traceStrcmpType = + MethodType.methodType(void.class, String.class, String.class, int.class, int.class); + traceStrcmp = MethodHandles.publicLookup().findStatic( + traceDataFlowNativeCallbacks, "traceStrcmp", traceStrcmpType); + MethodType traceStrstrType = + MethodType.methodType(void.class, String.class, String.class, int.class); + traceStrstr = MethodHandles.publicLookup().findStatic( + traceDataFlowNativeCallbacks, "traceStrstr", traceStrstrType); + MethodType traceMemcmpType = + MethodType.methodType(void.class, byte[].class, byte[].class, int.class, int.class); + traceMemcmp = MethodHandles.publicLookup().findStatic( + traceDataFlowNativeCallbacks, "traceMemcmp", traceMemcmpType); + MethodType tracePcIndirType = MethodType.methodType(void.class, int.class, int.class); + tracePcIndir = MethodHandles.publicLookup().findStatic( + traceDataFlowNativeCallbacks, "tracePcIndir", tracePcIndirType); + } catch (ClassNotFoundException ignore) { + // Not running in the context of the agent. This is fine as long as no methods are called on + // this class. + } catch (NoSuchMethodException | IllegalAccessException e) { + // This should never happen as the Jazzer API is loaded from the agent and thus should always + // match the version of the runtime classes. + System.err.println("ERROR: Incompatible version of the Jazzer API detected, please update."); + e.printStackTrace(); + System.exit(1); + } + JAZZER_INTERNAL = jazzerInternal; + ON_FUZZ_TARGET_READY = onFuzzTargetReady; + TRACE_STRCMP = traceStrcmp; + TRACE_STRSTR = traceStrstr; + TRACE_MEMCMP = traceMemcmp; + TRACE_PC_INDIR = tracePcIndir; + } + + private Jazzer() {} + + /** + * A 32-bit random number that hooks can use to make pseudo-random choices + * between multiple possible mutations they could guide the fuzzer towards. + * Hooks <b>must not</b> base the decision whether or not to report a finding + * on this number as this will make findings non-reproducible. + * <p> + * This is the same number that libFuzzer uses as a seed internally, which + * makes it possible to deterministically reproduce a previous fuzzing run by + * supplying the seed value printed by libFuzzer as the value of the + * {@code -seed}. + */ + public static final int SEED = getLibFuzzerSeed(); + + /** + * Instructs the fuzzer to guide its mutations towards making {@code current} equal to {@code + * target}. + * <p> + * If the relation between the raw fuzzer input and the value of {@code current} is relatively + * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to + * achieve equality. + * + * @param current a non-constant string observed during fuzz target execution + * @param target a string that {@code current} should become equal to, but currently isn't + * @param id a (probabilistically) unique identifier for this particular compare hint + */ + public static void guideTowardsEquality(String current, String target, int id) { + if (TRACE_STRCMP == null) { + return; + } + try { + TRACE_STRCMP.invokeExact(current, target, 1, id); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + /** + * Instructs the fuzzer to guide its mutations towards making {@code current} equal to {@code + * target}. + * <p> + * If the relation between the raw fuzzer input and the value of {@code current} is relatively + * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to + * achieve equality. + * + * @param current a non-constant byte array observed during fuzz target execution + * @param target a byte array that {@code current} should become equal to, but currently isn't + * @param id a (probabilistically) unique identifier for this particular compare hint + */ + public static void guideTowardsEquality(byte[] current, byte[] target, int id) { + if (TRACE_MEMCMP == null) { + return; + } + try { + TRACE_MEMCMP.invokeExact(current, target, 1, id); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + /** + * Instructs the fuzzer to guide its mutations towards making {@code haystack} contain {@code + * needle} as a substring. + * <p> + * If the relation between the raw fuzzer input and the value of {@code haystack} is relatively + * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to + * satisfy the substring check. + * + * @param haystack a non-constant string observed during fuzz target execution + * @param needle a string that should be contained in {@code haystack} as a substring, but + * currently isn't + * @param id a (probabilistically) unique identifier for this particular compare hint + */ + public static void guideTowardsContainment(String haystack, String needle, int id) { + if (TRACE_STRSTR == null) { + return; + } + try { + TRACE_STRSTR.invokeExact(haystack, needle, id); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + /** + * Instructs the fuzzer to attain as many possible values for the absolute value of {@code state} + * as possible. + * <p> + * Call this function from a fuzz target or a hook to help the fuzzer track partial progress + * (e.g. by passing the length of a common prefix of two lists that should become equal) or + * explore different values of state that is not directly related to code coverage (see the + * MazeFuzzer example). + * <p> + * <b>Note:</b> This hint only takes effect if the fuzzer is run with the argument + * {@code -use_value_profile=1}. + * + * @param state a numeric encoding of a state that should be varied by the fuzzer + * @param id a (probabilistically) unique identifier for this particular state hint + */ + public static void exploreState(byte state, int id) { + if (TRACE_PC_INDIR == null) { + return; + } + // We only use the lower 7 bits of state, which allows for 128 different state values tracked + // per id. The particular amount of 7 bits of state is also used in libFuzzer's + // TracePC::HandleCmp: + // https://github.com/llvm/llvm-project/blob/c12d49c4e286fa108d4d69f1c6d2b8d691993ffd/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp#L390 + // This value should be large enough for most use cases (e.g. tracking the length of a prefix in + // a comparison) while being small enough that the bitmap isn't filled up too quickly + // (65536 bits / 128 bits per id = 512 ids). + + // We use tracePcIndir as a way to set a bit in libFuzzer's value profile bitmap. In + // TracePC::HandleCallerCallee, which is what this function ultimately calls through to, the + // lower 12 bits of each argument are combined into a 24-bit index into the bitmap, which is + // then reduced modulo a 16-bit prime. To keep the modulo bias small, we should fill as many + // of the relevant bits as possible. + + // We pass state in the lowest bits of the caller address, which is used to form the lowest bits + // of the bitmap index. This should result in the best caching behavior as state is expected to + // change quickly in consecutive runs and in this way all its bitmap entries would be located + // close to each other in memory. + int lowerBits = (state & 0x7f) | (id << 7); + int upperBits = id >>> 5; + try { + TRACE_PC_INDIR.invokeExact(upperBits, lowerBits); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + /** + * Make Jazzer report the provided {@link Throwable} as a finding. + * <p> + * <b>Note:</b> This method must only be called from a method hook. In a + * fuzz target, simply throw an exception to trigger a finding. + * @param finding the finding that Jazzer should report + */ + public static void reportFindingFromHook(Throwable finding) { + try { + JAZZER_INTERNAL.getMethod("reportFindingFromHook", Throwable.class).invoke(null, finding); + } catch (NullPointerException | IllegalAccessException | NoSuchMethodException e) { + // We can only reach this point if the runtime is not on the classpath, e.g. in case of a + // reproducer. Just throw the finding. + rethrowUnchecked(finding); + } catch (InvocationTargetException e) { + rethrowUnchecked(e.getCause()); + } + } + + /** + * Register a callback to be executed right before the fuzz target is executed for the first time. + * <p> + * This can be used to disable hooks until after Jazzer has been fully initializing, e.g. to + * prevent Jazzer internals from triggering hooks on Java standard library classes. + * + * @param callback the callback to execute + */ + public static void onFuzzTargetReady(Runnable callback) { + try { + ON_FUZZ_TARGET_READY.invokeExact(callback); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + private static int getLibFuzzerSeed() { + // The Jazzer driver sets this property based on the value of libFuzzer's -seed command-line + // option, which allows for fully reproducible fuzzing runs if set. If not running in the + // context of the driver, fall back to a random number instead. + String rawSeed = System.getProperty("jazzer.internal.seed"); + if (rawSeed == null) { + return new SecureRandom().nextInt(); + } + // If jazzer.internal.seed is set, we expect it to be a valid integer. + return Integer.parseUnsignedInt(rawSeed); + } + + // Rethrows a (possibly checked) exception while avoiding a throws declaration. + @SuppressWarnings("unchecked") + private static <T extends Throwable> void rethrowUnchecked(Throwable t) throws T { + throw(T) t; + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java b/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java new file mode 100644 index 00000000..3a1c5f39 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java @@ -0,0 +1,207 @@ +// Copyright 2021 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.api; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.invoke.MethodType; + +/** + * Registers the annotated method as a hook that should run before, instead or + * after the method specified by the annotation parameters. + * <p> + * Depending on {@link #type()} this method will be called after, instead or + * before every call to the target method and has + * access to its parameters and return value. The target method is specified by + * {@link #targetClassName()} and {@link #targetMethod()}. In case of an + * overloaded method, {@link #targetMethodDescriptor()} can be used to restrict + * the application of the hook to a particular overload. + * <p> + * The signature of the annotated method must be as follows (this does not + * restrict the method name and parameter names, which are arbitrary), + * depending on the value of {@link #type()}: + * + * <dl> + * <dt><span class="strong">{@link HookType#BEFORE}</span> + * <dd> + * <pre>{@code + * public static void hook(MethodHandle method, Object thisObject, Object[] arguments, int hookId) + * }</pre> + * Arguments: + * <p><ul> + * <li>{@code method}: A {@link java.lang.invoke.MethodHandle} representing the + * original method. The original method can be invoked via + * {@link java.lang.invoke.MethodHandle#invokeWithArguments(Object...)}. This + * requires passing {@code thisObject} as the first argument if the method is + * not static. This argument can be {@code null}. + * <li>{@code thisObject}: An {@link Object} containing the implicit + * {@code this} argument to the original method. If the original method is + * static, this argument will be {@code null}. + * <li>{@code arguments}: An array of {@link Object}s containing the arguments + * passed to the original method. Primitive types (e.g. {@code boolean}) will be + * wrapped into their corresponding wrapper type (e.g. {@link Boolean}). + * <li>{@code hookId}: A random {@code int} identifying the particular call + * site.This can be used to derive additional coverage information. + * </ul> + * + * <dt><span class="strong">{@link HookType#REPLACE}</span> + * <dd> + * <pre>{@code + * public static Object hook(MethodHandle method, Object thisObject, Object[] arguments, int hookId) + * }</pre> + * The return type may alternatively be taken to be the exact return type of + * target method or a wrapper type thereof. The returned object will be casted + * and unwrapped automatically. + * <p> + * Arguments: + * <p><ul> + * <li>{@code method}: A {@link java.lang.invoke.MethodHandle} representing the + * original method. The original method can be invoked via + * {@link java.lang.invoke.MethodHandle#invokeWithArguments(Object...)}. This + * requires passing {@code thisObject} as the first argument if the method is + * not static. This argument can be {@code null}. + * <li>{@code thisObject}: An {@link Object} containing the implicit + * {@code this} argument to the original method. If the original method is + * static, this argument will be {@code null}. + * <li>{@code arguments}: An array of {@link Object}s containing the arguments + * passed to the original method. Primitive types (e.g. {@code boolean}) will be + * wrapped into their corresponding wrapper type (e.g. {@link Boolean}). + * <li>{@code hookId}: A random {@code int} identifying the particular call + * site.This can be used to derive additional coverage information. + * </ul><p> + * <p> + * Return value: the value that should take the role of the value the target + * method would have returned + * <p> + * <dt><span class="strong">{@link HookType#AFTER}</span> + * <dd> + * <pre>{@code + * public static void hook(MethodHandle method, Object thisObject, Object[] arguments, int hookId, + * Object returnValue) + * }</pre> + * Arguments: + * <p><ul> + * <li>{@code method}: A {@link java.lang.invoke.MethodHandle} representing the + * original method. The original method can be invoked via + * {@link java.lang.invoke.MethodHandle#invokeWithArguments(Object...)}. This + * requires passing {@code thisObject} as the first argument if the method is + * not static. This argument can be {@code null}. + * <li>{@code thisObject}: An {@link Object} containing the implicit + * {@code this} argument to the original method. If the original method is + * static, this argument will be {@code null}. + * <li>{@code arguments}: An array of {@link Object}s containing the arguments + * passed to the original method. Primitive types (e.g. {@code boolean}) will be + * wrapped into their corresponding wrapper type (e.g. {@link Boolean}). + * <li>{@code hookId}: A random {@code int} identifying the particular call + * site.This can be used to derive additional coverage information. + * <li>{@code returnValue}: An {@link Object} containing the return value of the + * invocation of the original method. Primitive types (e.g. {@code boolean}) + * will be wrapped into their corresponding wrapper type (e.g. {@link Boolean}). + * If the original method has return type {@code void}, this value will be + * {@code null}. + * <p> + * Multiple {@link HookType#BEFORE} and {@link HookType#AFTER} hooks are + * allowed to reference the same target method. Exclusively one + * {@link HookType#REPLACE} hook may reference a target method, no other types + * allowed. Attention must be paid to not guide the Fuzzer in different + * directions via {@link Jazzer}'s {@code guideTowardsXY} methods in the + * different hooks. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Repeatable(MethodHooks.class) +@Documented +public @interface MethodHook { + /** + * The time at which the annotated method should be called. + * <p> + * If this is {@link HookType#BEFORE}, the annotated method will be called + * before the target method and has access to its arguments. + * <p> + * If this is {@link HookType#REPLACE}, the annotated method will be called + * instead of the target method. It has access to its arguments and can + * return a value that will replace the target method's return value. + * <p> + * If this is {@link HookType#AFTER}, the annotated method will be called + * after the target method and has access to its arguments and return + * value. + * + * @return when the hook should be called + */ + HookType type(); + + /** + * The name of the class that contains the method that should be hooked, + * as returned by {@link Class#getName()}. + * <p> + * If an interface or abstract class is specified, also calls to all + * implementations and subclasses available on the classpath during startup + * are hooked, respectively. Interfaces and subclasses are not taken into + * account for concrete classes. + * <p> + * Examples: + * <p><ul> + * <li>{@link String}: {@code "java.lang.String"} + * <li>{@link java.nio.file.FileSystem}: {@code "java.nio.file.FileSystem"} + * </ul><p> + * + * @return the name of the class containing the method to be hooked + */ + String targetClassName(); + + /** + * The name of the method to be hooked. Use {@code "<init>"} for + * constructors. + * <p> + * Examples: + * <p><ul> + * <li>{@link String#equals(Object)}: {@code "equals"} + * <li>{@link String#String()}: {@code "<init>"} + * </ul><p> + * + * @return the name of the method to be hooked + */ + String targetMethod(); + + /** + * The descriptor of the method to be hooked. This is only needed if there + * are multiple methods with the same name and not all of them should be + * hooked. + * <p> + * The descriptor of a method is an internal representation of the method's + * signature, which includes the types of its parameters and its return + * value. For more information on descriptors, see the + * <a href=https://docs.oracle.com/javase/specs/jvms/se15/html/jvms-4.html#jvms-4.3.3>JVM + * Specification, Section 4.3.3</a> and {@link MethodType#toMethodDescriptorString()} + * + * @return the descriptor of the method to be hooked + */ + String targetMethodDescriptor() default ""; + + /** + * Array of additional classes to hook. + * <p> + * Hooks are applied on call sites. This means that classes calling the one + * defined in this annotation need to be instrumented to actually execute + * the hook. This property can be used to hook normally ignored classes. + * + * @return fully qualified class names to hook + */ + String[] additionalClassesToHook() default {}; +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/MethodHooks.java b/src/main/java/com/code_intelligence/jazzer/api/MethodHooks.java new file mode 100644 index 00000000..7eec24b3 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/MethodHooks.java @@ -0,0 +1,31 @@ +// Copyright 2021 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.api; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Internal helper allowing to apply multiple {@link MethodHook} annotations to the same method. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface MethodHooks { + MethodHook[] value(); +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/SilentCloseable.java b/src/main/java/com/code_intelligence/jazzer/api/SilentCloseable.java new file mode 100644 index 00000000..3f2d6e3f --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/SilentCloseable.java @@ -0,0 +1,25 @@ +/* + * 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.api; + +/** + * A specialization of {@link AutoCloseable} without a {@code throws} declarations on + * {@link #close()}. + */ +public interface SilentCloseable extends AutoCloseable { + @Override void close(); +} diff --git a/src/main/java/com/code_intelligence/jazzer/autofuzz/AccessibleObjectLookup.java b/src/main/java/com/code_intelligence/jazzer/autofuzz/AccessibleObjectLookup.java new file mode 100644 index 00000000..a45a474b --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/autofuzz/AccessibleObjectLookup.java @@ -0,0 +1,147 @@ +// Copyright 2022 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 io.github.classgraph.ClassInfo; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Comparator; +import java.util.stream.Stream; + +class AccessibleObjectLookup { + private static final Comparator<Class<?>> STABLE_CLASS_COMPARATOR = + Comparator.comparing(Class::getName); + private static final Comparator<Executable> STABLE_EXECUTABLE_COMPARATOR = + Comparator.comparing(Executable::getName).thenComparing(executable -> { + if (executable instanceof Method) { + return org.objectweb.asm.Type.getMethodDescriptor((Method) executable); + } else { + return org.objectweb.asm.Type.getConstructorDescriptor((Constructor<?>) executable); + } + }); + + private final Class<?> referenceClass; + + public AccessibleObjectLookup(Class<?> referenceClass) { + this.referenceClass = referenceClass; + } + + Class<?>[] getAccessibleClasses(Class<?> type) { + return Stream.concat(Arrays.stream(type.getDeclaredClasses()), Arrays.stream(type.getClasses())) + .distinct() + .filter(this::isAccessible) + .sorted(STABLE_CLASS_COMPARATOR) + .toArray(Class<?>[] ::new); + } + + Constructor<?>[] getAccessibleConstructors(Class<?> type) { + // Neither of getDeclaredConstructors and getConstructors is a superset of the other: While + // getDeclaredConstructors returns constructors with all visibility modifiers, it does not + // return the implicit default constructor. + return Stream + .concat( + Arrays.stream(type.getDeclaredConstructors()), Arrays.stream(type.getConstructors())) + .distinct() + .filter(this::isAccessible) + .sorted(STABLE_EXECUTABLE_COMPARATOR) + .filter(constructor -> { + try { + constructor.setAccessible(true); + return true; + } catch (Exception e) { + // Can't make the constructor accessible, e.g. because it is in a standard library + // module. We can't do anything about this, so we skip the constructor. + return false; + } + }) + .toArray(Constructor<?>[] ::new); + } + + Method[] getAccessibleMethods(Class<?> type) { + return Stream.concat(Arrays.stream(type.getDeclaredMethods()), Arrays.stream(type.getMethods())) + .distinct() + .filter(this::isAccessible) + .sorted(STABLE_EXECUTABLE_COMPARATOR) + .filter(method -> { + try { + method.setAccessible(true); + return true; + } catch (Exception e) { + // Can't make the method accessible, e.g. because it is in a standard library module. We + // can't do anything about this, so we skip the method. + return false; + } + }) + .toArray(Method[] ::new); + } + + boolean isAccessible(Class<?> clazz, int modifiers) { + if (Modifier.isPublic(modifiers)) { + return true; + } + if (referenceClass == null) { + return false; + } + if (Modifier.isPrivate(modifiers)) { + return clazz.equals(referenceClass); + } + if (Modifier.isProtected(modifiers)) { + return clazz.isAssignableFrom(referenceClass); + } + // No visibility modifiers implies default visibility, which means visible in the same package. + return clazz.getPackage().equals(referenceClass.getPackage()); + } + + boolean isAccessible(ClassInfo clazz, int modifiers) { + if (Modifier.isPublic(modifiers)) { + return true; + } + if (referenceClass == null) { + return false; + } + if (Modifier.isPrivate(modifiers)) { + return clazz.getName().equals(referenceClass.getName()); + } + if (Modifier.isProtected(modifiers)) { + return isAssignableFrom(clazz, referenceClass); + } + // No visibility modifiers implies default visibility, which means visible in the same package. + return clazz.getPackageName().equals(referenceClass.getPackage().getName()); + } + + boolean isAssignableFrom(ClassInfo clazz, Class<?> potentialSubclass) { + if (potentialSubclass.getName().equals(clazz.getName())) { + return true; + } + if (potentialSubclass.equals(Object.class)) { + return clazz.getName().equals(Object.class.getName()); + } + if (potentialSubclass.getSuperclass() == null) { + return false; + } + return isAssignableFrom(clazz, potentialSubclass.getSuperclass()); + } + + private boolean isAccessible(Executable executable) { + return isAccessible(executable.getDeclaringClass(), executable.getModifiers()); + } + + private boolean isAccessible(Class<?> clazz) { + return isAccessible(clazz, clazz.getModifiers()); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzCodegenVisitor.java b/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzCodegenVisitor.java new file mode 100644 index 00000000..2ea4e9b0 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzCodegenVisitor.java @@ -0,0 +1,116 @@ +// Copyright 2021 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 java.util.Stack; +import java.util.stream.Collectors; + +public class AutofuzzCodegenVisitor { + private final Stack<Group> groups = new Stack<>(); + private int variableCounter = 0; + + AutofuzzCodegenVisitor() { + init(); + } + + private void init() { + pushGroup("", "", ""); + } + + public void pushGroup(String prefix, String delimiter, String suffix) { + groups.push(new Group(prefix, delimiter, suffix)); + } + + public void pushElement(String element) { + groups.peek().push(element); + } + + public void popElement() { + groups.peek().pop(); + } + + public void popGroup() { + if (groups.size() == 1) { + throw new AutofuzzError( + "popGroup must be called exactly once for every pushGroup: " + toDebugString()); + } + pushElement(groups.pop().toString()); + } + + public String generate() { + if (groups.size() != 1) { + throw new AutofuzzError( + "popGroup must be called exactly once for every pushGroup: " + toDebugString()); + } + return groups.pop().toString(); + } + + public void addCharLiteral(char c) { + pushElement("'" + escapeForLiteral(Character.toString(c)) + "'"); + } + + public void addStringLiteral(String string) { + pushElement('"' + escapeForLiteral(string) + '"'); + } + + public String uniqueVariableName() { + return String.format("autofuzzVariable%s", variableCounter++); + } + + static String escapeForLiteral(String string) { + // The list of escape sequences is taken from: + // https://docs.oracle.com/javase/tutorial/java/data/characters.html + return string.replace("\\", "\\\\") + .replace("\t", "\\t") + .replace("\b", "\\b") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\f", "\\f") + .replace("\"", "\\\"") + .replace("'", "\\'"); + } + + private String toDebugString() { + return groups.stream() + .map(group -> group.elements.stream().collect(Collectors.joining(", ", "[", "]"))) + .collect(Collectors.joining(", ", "[", "]")); + } + + private static class Group { + private final String prefix; + private final String delimiter; + private final String suffix; + private final Stack<String> elements = new Stack<>(); + + Group(String prefix, String delimiter, String suffix) { + this.prefix = prefix; + this.delimiter = delimiter; + this.suffix = suffix; + } + + public void push(String element) { + elements.push(element); + } + + public void pop() { + elements.pop(); + } + + @Override + public String toString() { + return elements.stream().collect(Collectors.joining(delimiter, prefix, suffix)); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzError.java b/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzError.java new file mode 100644 index 00000000..a94b385d --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/autofuzz/AutofuzzError.java @@ -0,0 +1,31 @@ +// Copyright 2021 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; + +/** + * An error indicating an internal error in the autofuzz functionality. + */ +public class AutofuzzError extends Error { + private static final String MESSAGE_TRAILER = String.format( + "%nPlease file an issue at:%n https://github.com/CodeIntelligenceTesting/jazzer/issues/new/choose"); + + public AutofuzzError(String message) { + super(message + MESSAGE_TRAILER); + } + + public AutofuzzError(String message, Throwable cause) { + super(message + MESSAGE_TRAILER, cause); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel new file mode 100644 index 00000000..04a076e6 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel @@ -0,0 +1,22 @@ +java_library( + name = "autofuzz", + srcs = [ + "AccessibleObjectLookup.java", + "AutofuzzCodegenVisitor.java", + "AutofuzzError.java", + "FuzzTarget.java", + "Meta.java", + "YourAverageJavaClass.java", + ], + visibility = ["//visibility:public"], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/api", + "//src/main/java/com/code_intelligence/jazzer/runtime:jazzer_bootstrap_compile_only", + "//src/main/java/com/code_intelligence/jazzer/utils", + "//src/main/java/com/code_intelligence/jazzer/utils:log", + "//src/main/java/com/code_intelligence/jazzer/utils:simple_glob_matcher", + "@com_github_classgraph_classgraph//:classgraph", + "@com_github_jhalterman_typetools//:typetools", + "@org_ow2_asm_asm//jar", + ], +) 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(); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java b/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java new file mode 100644 index 00000000..543284f1 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java @@ -0,0 +1,809 @@ +// Copyright 2021 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.Consumer1; +import com.code_intelligence.jazzer.api.Consumer2; +import com.code_intelligence.jazzer.api.Consumer3; +import com.code_intelligence.jazzer.api.Consumer4; +import com.code_intelligence.jazzer.api.Consumer5; +import com.code_intelligence.jazzer.api.Function1; +import com.code_intelligence.jazzer.api.Function2; +import com.code_intelligence.jazzer.api.Function3; +import com.code_intelligence.jazzer.api.Function4; +import com.code_intelligence.jazzer.api.Function5; +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.runtime.HardToCatchError; +import com.code_intelligence.jazzer.utils.Utils; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.ScanResult; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import net.jodah.typetools.TypeResolver; +import net.jodah.typetools.TypeResolver.Unknown; + +public class Meta { + public static final boolean IS_DEBUG = isDebug(); + + private static final Meta PUBLIC_LOOKUP_INSTANCE = new Meta(null); + private static final boolean IS_TEST = isTest(); + private static final WeakHashMap<Class<?>, List<Class<?>>> implementingClassesCache = + new WeakHashMap<>(); + private static final WeakHashMap<Class<?>, List<Class<?>>> nestedBuilderClassesCache = + new WeakHashMap<>(); + private static final WeakHashMap<Class<?>, List<Method>> originalObjectCreationMethodsCache = + new WeakHashMap<>(); + private static final WeakHashMap<Class<?>, List<Method>> cascadingBuilderMethodsCache = + new WeakHashMap<>(); + + private final AccessibleObjectLookup lookup; + + public Meta(Class<?> referenceClass) { + lookup = new AccessibleObjectLookup(referenceClass); + } + + @SuppressWarnings("unchecked") + public static <T1> void autofuzz(FuzzedDataProvider data, Consumer1<T1> func) { + Class<?>[] types = TypeResolver.resolveRawArguments(Consumer1.class, func.getClass()); + func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0)); + } + + @SuppressWarnings("unchecked") + public static <T1, T2> void autofuzz(FuzzedDataProvider data, Consumer2<T1, T2> func) { + Class<?>[] types = TypeResolver.resolveRawArguments(Consumer2.class, func.getClass()); + func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0), + (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1)); + } + + @SuppressWarnings("unchecked") + public static <T1, T2, T3> void autofuzz(FuzzedDataProvider data, Consumer3<T1, T2, T3> func) { + Class<?>[] types = TypeResolver.resolveRawArguments(Consumer3.class, func.getClass()); + func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0), + (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1), + (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2)); + } + + @SuppressWarnings("unchecked") + public static <T1, T2, T3, T4> void autofuzz( + FuzzedDataProvider data, Consumer4<T1, T2, T3, T4> func) { + Class<?>[] types = TypeResolver.resolveRawArguments(Consumer4.class, func.getClass()); + func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0), + (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1), + (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2), + (T4) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 3)); + } + + @SuppressWarnings("unchecked") + public static <T1, T2, T3, T4, T5> void autofuzz( + FuzzedDataProvider data, Consumer5<T1, T2, T3, T4, T5> func) { + Class<?>[] types = TypeResolver.resolveRawArguments(Consumer5.class, func.getClass()); + func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0), + (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1), + (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2), + (T4) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 3), + (T5) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 4)); + } + + @SuppressWarnings("unchecked") + public static <T1, R> R autofuzz(FuzzedDataProvider data, Function1<T1, R> func) { + Class<?>[] types = TypeResolver.resolveRawArguments(Function1.class, func.getClass()); + return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0)); + } + + @SuppressWarnings("unchecked") + public static <T1, T2, R> R autofuzz(FuzzedDataProvider data, Function2<T1, T2, R> func) { + Class<?>[] types = TypeResolver.resolveRawArguments(Function2.class, func.getClass()); + return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0), + (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1)); + } + + @SuppressWarnings("unchecked") + public static <T1, T2, T3, R> R autofuzz(FuzzedDataProvider data, Function3<T1, T2, T3, R> func) { + Class<?>[] types = TypeResolver.resolveRawArguments(Function3.class, func.getClass()); + return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0), + (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1), + (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2)); + } + + @SuppressWarnings("unchecked") + public static <T1, T2, T3, T4, R> R autofuzz( + FuzzedDataProvider data, Function4<T1, T2, T3, T4, R> func) { + Class<?>[] types = TypeResolver.resolveRawArguments(Function4.class, func.getClass()); + return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0), + (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1), + (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2), + (T4) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 3)); + } + + @SuppressWarnings("unchecked") + public static <T1, T2, T3, T4, T5, R> R autofuzz( + FuzzedDataProvider data, Function5<T1, T2, T3, T4, T5, R> func) { + Class<?>[] types = TypeResolver.resolveRawArguments(Function5.class, func.getClass()); + return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0), + (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1), + (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2), + (T4) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 3), + (T5) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 4)); + } + + public static Object consume(FuzzedDataProvider data, Class<?> type) { + return PUBLIC_LOOKUP_INSTANCE.consume(data, type, null); + } + + static void rescanClasspath() { + implementingClassesCache.clear(); + } + + private static boolean isTest() { + String value = System.getenv("JAZZER_AUTOFUZZ_TESTING"); + return value != null && !value.isEmpty(); + } + + private static boolean isDebug() { + String value = System.getenv("JAZZER_AUTOFUZZ_DEBUG"); + return value != null && !value.isEmpty(); + } + + private static int consumeArrayLength(FuzzedDataProvider data, int sizeOfElement) { + // Spend at most half of the fuzzer input bytes so that the remaining arguments that require + // construction still have non-trivial data to work with. + int bytesToSpend = data.remainingBytes() / 2; + return bytesToSpend / Math.max(sizeOfElement, 1); + } + + private static String deepToString(Object obj) { + if (obj == null) { + return "null"; + } + if (obj.getClass().isArray()) { + return String.format("(%s[]) %s", obj.getClass().getComponentType().getName(), + Arrays.deepToString((Object[]) obj)); + } + return obj.toString(); + } + + private static String getDebugSummary( + Executable executable, Object thisObject, Object[] arguments) { + return String.format("%nMethod: %s::%s%s%nthis: %s%nArguments: %s", + executable.getDeclaringClass().getName(), executable.getName(), + Utils.getReadableDescriptor(executable), thisObject, + Arrays.stream(arguments).map(Meta::deepToString).collect(Collectors.joining(", "))); + } + + static Class<?> getRawType(Type genericType) { + if (genericType instanceof Class<?>) { + return (Class<?>) genericType; + } else if (genericType instanceof ParameterizedType) { + return getRawType(((ParameterizedType) genericType).getRawType()); + } else if (genericType instanceof WildcardType) { + // TODO: Improve this. + return Object.class; + } else if (genericType instanceof TypeVariable<?>) { + throw new AutofuzzError("Did not expect genericType to be a TypeVariable: " + genericType); + } else if (genericType instanceof GenericArrayType) { + return Array + .newInstance(getRawType(((GenericArrayType) genericType).getGenericComponentType()), 0) + .getClass(); + } else { + throw new AutofuzzError("Got unexpected class implementing Type: " + genericType); + } + } + + public Object autofuzz(FuzzedDataProvider data, Method method) { + return autofuzz(data, method, null); + } + + // Renamed so that it doesn't clash with the static method consume, which we don't want to rename + // as the api package depends on it by name. + public Object consumeNonStatic(FuzzedDataProvider data, Class<?> type) { + return consume(data, type, null); + } + + Object autofuzz(FuzzedDataProvider data, Method method, AutofuzzCodegenVisitor visitor) { + Object result; + if (Modifier.isStatic(method.getModifiers())) { + if (visitor != null) { + // This group will always have two elements: The class name and the method call. + visitor.pushGroup( + String.format("%s.", method.getDeclaringClass().getCanonicalName()), "", ""); + } + try { + result = autofuzz(data, method, null, visitor); + } finally { + if (visitor != null) { + visitor.popGroup(); + } + } + } else { + if (visitor != null) { + // This group will always have two elements: The thisObject and the method call. + // Since the this object can be a complex expression, wrap it in parenthesis. + visitor.pushGroup("(", ").", ""); + } + try { + Object thisObject = consume(data, method.getDeclaringClass(), visitor); + if (thisObject == null) { + throw new AutofuzzConstructionException(); + } + result = autofuzz(data, method, thisObject, visitor); + } finally { + if (visitor != null) { + visitor.popGroup(); + } + } + } + return result; + } + + public Object autofuzz(FuzzedDataProvider data, Method method, Object thisObject) { + return autofuzz(data, method, thisObject, null); + } + + Object autofuzz( + FuzzedDataProvider data, Method method, Object thisObject, AutofuzzCodegenVisitor visitor) { + if (visitor != null) { + visitor.pushGroup(String.format("%s(", method.getName()), ", ", ")"); + } + Object[] arguments = consumeArguments(data, method, visitor); + if (visitor != null) { + visitor.popGroup(); + } + try { + return method.invoke(thisObject, arguments); + } catch (IllegalAccessException | IllegalArgumentException | NullPointerException e) { + // We should ensure that the arguments fed into the method are always valid. + throw new AutofuzzError(getDebugSummary(method, thisObject, arguments), e); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof HardToCatchError) { + throw new AutofuzzInvocationException(); + } + throw new AutofuzzInvocationException(e.getCause()); + } + } + + Object autofuzzForConsume( + FuzzedDataProvider data, Constructor<?> constructor, AutofuzzCodegenVisitor visitor) { + try { + return autofuzz(data, constructor, visitor); + } catch (AutofuzzConstructionException e) { + // Do not nest AutofuzzConstructionExceptions. + throw e; + } catch (AutofuzzInvocationException e) { + // If an invocation fails during consume and thus while trying to construct a valid object, + // the exception should not be reported as a finding, so we rewrap it. + throw new AutofuzzConstructionException(e.getCause()); + } catch (Throwable t) { + throw new AutofuzzConstructionException(t); + } + } + + Object autofuzzForConsume( + FuzzedDataProvider data, Method method, Object thisObject, AutofuzzCodegenVisitor visitor) { + try { + return autofuzz(data, method, thisObject, visitor); + } catch (AutofuzzConstructionException e) { + // Do not nest AutofuzzConstructionExceptions. + throw e; + } catch (AutofuzzInvocationException e) { + // If an invocation fails during consume and thus while trying to construct a valid object, + // the exception should not be reported as a finding, so we rewrap it. + throw new AutofuzzConstructionException(e.getCause()); + } catch (Throwable t) { + throw new AutofuzzConstructionException(t); + } + } + + public <R> R autofuzz(FuzzedDataProvider data, Constructor<R> constructor) { + return autofuzz(data, constructor, null); + } + + <R> R autofuzz( + FuzzedDataProvider data, Constructor<R> constructor, AutofuzzCodegenVisitor visitor) { + if (visitor != null) { + // getCanonicalName is correct also for nested classes. + visitor.pushGroup( + String.format("new %s(", constructor.getDeclaringClass().getCanonicalName()), ", ", ")"); + } + Object[] arguments = consumeArguments(data, constructor, visitor); + if (visitor != null) { + visitor.popGroup(); + } + try { + return constructor.newInstance(arguments); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException e) { + // This should never be reached as the logic in consume should prevent us from e.g. calling + // constructors of abstract classes or private constructors. + throw new AutofuzzError(getDebugSummary(constructor, null, arguments), e); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof HardToCatchError) { + throw new AutofuzzInvocationException(); + } + throw new AutofuzzInvocationException(e.getCause()); + } + } + + // Invariant: The Java source code representation of the returned object visited by visitor must + // represent an object of the same type as genericType. For example, a null value returned for + // the genericType Class<java.lang.String> should lead to the generated code + // "(java.lang.String) null", not just "null". This makes it possible to safely use consume in + // recursive argument constructions. + // Exception: Some Java libraries offer public methods that take private interfaces or abstract + // classes as parameters. In this case, a cast to the parent type would cause an + // IllegalAccessError. Since this case should be rare and there is no good alternative to + // disambiguate overloads, we omit the cast in this case. + Object consume(FuzzedDataProvider data, Type genericType, AutofuzzCodegenVisitor visitor) { + Class<?> type = getRawType(genericType); + if (type == byte.class || type == Byte.class) { + byte result = data.consumeByte(); + if (visitor != null) { + visitor.pushElement(String.format("(byte) %s", result)); + } + return result; + } else if (type == short.class || type == Short.class) { + short result = data.consumeShort(); + if (visitor != null) { + visitor.pushElement(String.format("(short) %s", result)); + } + return result; + } else if (type == int.class || type == Integer.class) { + int result = data.consumeInt(); + if (visitor != null) { + visitor.pushElement(Integer.toString(result)); + } + return result; + } else if (type == long.class || type == Long.class) { + long result = data.consumeLong(); + if (visitor != null) { + visitor.pushElement(String.format("%sL", result)); + } + return result; + } else if (type == float.class || type == Float.class) { + float result = data.consumeFloat(); + if (visitor != null) { + visitor.pushElement(String.format("%sF", result)); + } + return result; + } else if (type == double.class || type == Double.class) { + double result = data.consumeDouble(); + if (visitor != null) { + visitor.pushElement(Double.toString(result)); + } + return result; + } else if (type == boolean.class || type == Boolean.class) { + boolean result = data.consumeBoolean(); + if (visitor != null) { + visitor.pushElement(Boolean.toString(result)); + } + return result; + } else if (type == char.class || type == Character.class) { + char result = data.consumeChar(); + if (visitor != null) { + visitor.addCharLiteral(result); + } + return result; + } + // Sometimes, but rarely return null for non-primitive and non-boxed types. + // TODO: We might want to return null for boxed types sometimes, but this is complicated by the + // fact that TypeUtils can't distinguish between a primitive type and its wrapper and may + // thus easily cause false-positive NullPointerExceptions. + if (!type.isPrimitive() && data.consumeByte() == 0) { + if (visitor != null) { + if (type == Object.class) { + visitor.pushElement("null"); + } else { + visitor.pushElement(String.format("(%s) null", type.getCanonicalName())); + } + } + return null; + } + if (type == String.class || type == CharSequence.class) { + String result = data.consumeString(consumeArrayLength(data, 1)); + if (visitor != null) { + visitor.addStringLiteral(result); + } + return result; + } else if (type.isArray()) { + if (type == byte[].class) { + byte[] result = data.consumeBytes(consumeArrayLength(data, Byte.BYTES)); + if (visitor != null) { + visitor.pushElement(IntStream.range(0, result.length) + .mapToObj(i -> "(byte) " + result[i]) + .collect(Collectors.joining(", ", "new byte[]{", "}"))); + } + return result; + } else if (type == int[].class) { + int[] result = data.consumeInts(consumeArrayLength(data, Integer.BYTES)); + if (visitor != null) { + visitor.pushElement(Arrays.stream(result) + .mapToObj(String::valueOf) + .collect(Collectors.joining(", ", "new int[]{", "}"))); + } + return result; + } else if (type == short[].class) { + short[] result = data.consumeShorts(consumeArrayLength(data, Short.BYTES)); + if (visitor != null) { + visitor.pushElement(IntStream.range(0, result.length) + .mapToObj(i -> "(short) " + result[i]) + .collect(Collectors.joining(", ", "new short[]{", "}"))); + } + return result; + } else if (type == long[].class) { + long[] result = data.consumeLongs(consumeArrayLength(data, Long.BYTES)); + if (visitor != null) { + visitor.pushElement(Arrays.stream(result) + .mapToObj(e -> e + "L") + .collect(Collectors.joining(", ", "new long[]{", "}"))); + } + return result; + } else if (type == boolean[].class) { + boolean[] result = data.consumeBooleans(consumeArrayLength(data, 1)); + if (visitor != null) { + visitor.pushElement( + Arrays.toString(result).replace(']', '}').replace("[", "new boolean[]{")); + } + return result; + } else { + if (visitor != null) { + visitor.pushGroup( + String.format("new %s[]{", type.getComponentType().getName()), ", ", "}"); + } + int remainingBytesBeforeFirstElementCreation = data.remainingBytes(); + Object firstElement = consume(data, type.getComponentType(), visitor); + int remainingBytesAfterFirstElementCreation = data.remainingBytes(); + int sizeOfElementEstimate = + remainingBytesBeforeFirstElementCreation - remainingBytesAfterFirstElementCreation; + Object array = Array.newInstance( + type.getComponentType(), consumeArrayLength(data, sizeOfElementEstimate)); + for (int i = 0; i < Array.getLength(array); i++) { + if (i == 0) { + Array.set(array, i, firstElement); + } else { + Array.set(array, i, consume(data, type.getComponentType(), visitor)); + } + } + if (visitor != null) { + if (Array.getLength(array) == 0) { + // We implicitly pushed the first element with the call to consume above, but it is not + // part of the array. + visitor.popElement(); + } + visitor.popGroup(); + } + return array; + } + } else if (type == ByteArrayInputStream.class || type == InputStream.class) { + byte[] array = data.consumeBytes(consumeArrayLength(data, Byte.BYTES)); + if (visitor != null) { + visitor.pushElement(IntStream.range(0, array.length) + .mapToObj(i -> "(byte) " + array[i]) + .collect(Collectors.joining( + ", ", "new java.io.ByteArrayInputStream(new byte[]{", "})"))); + } + return new ByteArrayInputStream(array); + } else if (type == Map.class) { + ParameterizedType mapType = (ParameterizedType) genericType; + if (mapType.getActualTypeArguments().length != 2) { + throw new AutofuzzError( + "Expected Map generic type to have two type parameters: " + mapType); + } + Type keyType = mapType.getActualTypeArguments()[0]; + Type valueType = mapType.getActualTypeArguments()[1]; + if (visitor != null) { + // Do not use Collectors.toMap() since it cannot handle null values. + // Also annotate the type of the entry stream since it might be empty, in which case type + // inference on the accumulator could fail. + visitor.pushGroup( + String.format("java.util.stream.Stream.<java.util.AbstractMap.SimpleEntry<%s, %s>>of(", + keyType.getTypeName(), valueType.getTypeName()), + ", ", + ").collect(java.util.HashMap::new, (map, e) -> map.put(e.getKey(), e.getValue()), java.util.HashMap::putAll)"); + } + int remainingBytesBeforeFirstEntryCreation = data.remainingBytes(); + if (visitor != null) { + visitor.pushGroup("new java.util.AbstractMap.SimpleEntry<>(", ", ", ")"); + } + Object firstKey = consume(data, keyType, visitor); + Object firstValue = consume(data, valueType, visitor); + if (visitor != null) { + visitor.popGroup(); + } + int remainingBytesAfterFirstEntryCreation = data.remainingBytes(); + int sizeOfElementEstimate = + remainingBytesBeforeFirstEntryCreation - remainingBytesAfterFirstEntryCreation; + int mapSize = consumeArrayLength(data, sizeOfElementEstimate); + Map<Object, Object> map = new HashMap<>(mapSize); + for (int i = 0; i < mapSize; i++) { + if (i == 0) { + map.put(firstKey, firstValue); + } else { + if (visitor != null) { + visitor.pushGroup("new java.util.AbstractMap.SimpleEntry<>(", ", ", ")"); + } + map.put(consume(data, keyType, visitor), consume(data, valueType, visitor)); + if (visitor != null) { + visitor.popGroup(); + } + } + } + if (visitor != null) { + if (mapSize == 0) { + // We implicitly pushed the first entry with the call to consume above, but it is not + // part of the array. + visitor.popElement(); + } + visitor.popGroup(); + } + return map; + } else if (type.isEnum()) { + Enum<?> enumValue = (Enum<?>) data.pickValue(type.getEnumConstants()); + if (visitor != null) { + visitor.pushElement(String.format("%s.%s", type.getName(), enumValue.name())); + } + return enumValue; + } else if (type == Class.class) { + if (visitor != null) { + visitor.pushElement(String.format("%s.class", YourAverageJavaClass.class.getName())); + } + return YourAverageJavaClass.class; + } else if (type == Method.class) { + if (visitor != null) { + throw new AutofuzzError("codegen has not been implemented for Method.class"); + } + return data.pickValue(lookup.getAccessibleMethods(YourAverageJavaClass.class)); + } else if (type == Constructor.class) { + if (visitor != null) { + throw new AutofuzzError("codegen has not been implemented for Constructor.class"); + } + return data.pickValue(lookup.getAccessibleConstructors(YourAverageJavaClass.class)); + } else if (type.isInterface() || Modifier.isAbstract(type.getModifiers())) { + List<Class<?>> implementingClasses = implementingClassesCache.get(type); + if (implementingClasses == null) { + // TODO: We may be scanning multiple times. Instead, we should keep the ScanResult around + // for as long as there is enough memory. + ClassGraph classGraph = new ClassGraph() + .enableClassInfo() + .ignoreClassVisibility() + .ignoreMethodVisibility() + .enableInterClassDependencies() + .rejectPackages("jaz"); + if (!IS_TEST) { + classGraph.rejectPackages("com.code_intelligence.jazzer"); + } + try (ScanResult result = classGraph.scan()) { + ClassInfoList children = + type.isInterface() ? result.getClassesImplementing(type) : result.getSubclasses(type); + implementingClasses = children.getStandardClasses() + .filter(info -> !Modifier.isAbstract(info.getModifiers())) + .filter(info -> lookup.isAccessible(info, info.getModifiers())) + // Filter out anonymous and local classes, which can't be + // instantiated in reproducers. + .filter(info -> info.getName() != null) + .loadClasses(); + implementingClassesCache.put(type, implementingClasses); + } + } + if (implementingClasses.isEmpty()) { + if (IS_DEBUG) { + throw new AutofuzzConstructionException(String.format( + "Could not find classes implementing %s on the classpath", type.getName())); + } else { + throw new AutofuzzConstructionException(); + } + } + if (visitor != null) { + // See the "Exception" note in the method comment. + if (Modifier.isPublic(type.getModifiers())) { + // This group will always have a single element: The instance of the implementing class. + visitor.pushGroup(String.format("(%s) ", type.getCanonicalName()), "", ""); + } + } + Object result = consume(data, data.pickValue(implementingClasses), visitor); + if (visitor != null) { + if (Modifier.isPublic(type.getModifiers())) { + visitor.popGroup(); + } + } + return result; + } + Constructor<?>[] constructors = lookup.getAccessibleConstructors(type); + if (constructors.length > 0) { + Constructor<?> constructor = data.pickValue(constructors); + boolean applySetters = constructor.getParameterCount() == 0; + if (visitor != null && applySetters) { + // Embed the instance creation and setters into an immediately invoked lambda expression to + // turn them into an expression. + String uniqueVariableName = visitor.uniqueVariableName(); + visitor.pushGroup(String.format("((java.util.function.Supplier<%1$s>) (() -> {%1$s %2$s = ", + type.getCanonicalName(), uniqueVariableName), + String.format("; %s.", uniqueVariableName), + String.format("; return %s;})).get()", uniqueVariableName)); + } + Object obj = autofuzzForConsume(data, constructor, visitor); + if (applySetters) { + List<Method> potentialSetters = getPotentialSetters(type); + if (!potentialSetters.isEmpty()) { + List<Method> pickedSetters = + data.pickValues(potentialSetters, data.consumeInt(0, potentialSetters.size())); + for (Method setter : pickedSetters) { + autofuzzForConsume(data, setter, obj, visitor); + } + } + if (visitor != null) { + visitor.popGroup(); + } + } + return obj; + } + // We are out of more or less canonical ways to construct an instance of this class and have to + // resort to more heuristic approaches. + + // First, try to find nested classes with names ending in Builder and call a subset of their + // chaining methods. + List<Class<?>> nestedBuilderClasses = getNestedBuilderClasses(type); + if (!nestedBuilderClasses.isEmpty()) { + Class<?> pickedBuilder = data.pickValue(nestedBuilderClasses); + List<Method> cascadingBuilderMethods = getCascadingBuilderMethods(pickedBuilder); + List<Method> originalObjectCreationMethods = getOriginalObjectCreationMethods(pickedBuilder); + + int pickedMethodsNumber = data.consumeInt(0, cascadingBuilderMethods.size()); + List<Method> pickedMethods = data.pickValues(cascadingBuilderMethods, pickedMethodsNumber); + Method builderMethod = data.pickValue(originalObjectCreationMethods); + + if (visitor != null) { + // Group for the chain of builder methods. + visitor.pushGroup("", ".", ""); + } + Object builderObj = autofuzzForConsume( + data, data.pickValue(lookup.getAccessibleConstructors(pickedBuilder)), visitor); + for (Method method : pickedMethods) { + builderObj = autofuzzForConsume(data, method, builderObj, visitor); + } + + try { + Object obj = autofuzzForConsume(data, builderMethod, builderObj, visitor); + if (visitor != null) { + visitor.popGroup(); + } + return obj; + } catch (Exception e) { + throw new AutofuzzConstructionException(e); + } + } + + // We ran out of ways to construct an instance of the requested type. If in debug mode, report + // more detailed information. + if (IS_DEBUG) { + String summary = String.format( + "Failed to generate instance of %s:%nAccessible constructors: %s%nNested subclasses: %s%n", + type.getName(), + Arrays.stream(lookup.getAccessibleConstructors(type)) + .map(Utils::getReadableDescriptor) + .collect(Collectors.joining(", ")), + Arrays.stream(lookup.getAccessibleClasses(type)) + .map(Class::getName) + .collect(Collectors.joining(", "))); + throw new AutofuzzConstructionException(summary); + } else { + throw new AutofuzzConstructionException(); + } + } + + private List<Class<?>> getNestedBuilderClasses(Class<?> type) { + List<Class<?>> nestedBuilderClasses = nestedBuilderClassesCache.get(type); + if (nestedBuilderClasses == null) { + nestedBuilderClasses = Arrays.stream(lookup.getAccessibleClasses(type)) + .filter(cls -> cls.getName().endsWith("Builder")) + .filter(cls -> !getOriginalObjectCreationMethods(cls).isEmpty()) + .collect(Collectors.toList()); + nestedBuilderClassesCache.put(type, nestedBuilderClasses); + } + return nestedBuilderClasses; + } + + private List<Method> getOriginalObjectCreationMethods(Class<?> builder) { + List<Method> originalObjectCreationMethods = originalObjectCreationMethodsCache.get(builder); + if (originalObjectCreationMethods == null) { + originalObjectCreationMethods = + Arrays.stream(lookup.getAccessibleMethods(builder)) + .filter(m -> m.getReturnType() == builder.getEnclosingClass()) + .collect(Collectors.toList()); + originalObjectCreationMethodsCache.put(builder, originalObjectCreationMethods); + } + return originalObjectCreationMethods; + } + + private List<Method> getCascadingBuilderMethods(Class<?> builder) { + List<Method> cascadingBuilderMethods = cascadingBuilderMethodsCache.get(builder); + if (cascadingBuilderMethods == null) { + cascadingBuilderMethods = Arrays.stream(lookup.getAccessibleMethods(builder)) + .filter(m -> m.getReturnType() == builder) + .collect(Collectors.toList()); + cascadingBuilderMethodsCache.put(builder, cascadingBuilderMethods); + } + return cascadingBuilderMethods; + } + + private List<Method> getPotentialSetters(Class<?> type) { + return Arrays.stream(lookup.getAccessibleMethods(type)) + .filter(method -> void.class.equals(method.getReturnType())) + .filter(method -> method.getParameterCount() == 1) + .filter(method -> method.getName().startsWith("set")) + .collect(Collectors.toList()); + } + + public Object[] consumeArguments( + FuzzedDataProvider data, Executable executable, AutofuzzCodegenVisitor visitor) { + Object[] result; + try { + result = Arrays.stream(executable.getGenericParameterTypes()) + .map(type -> consume(data, type, visitor)) + .toArray(); + return result; + } catch (AutofuzzConstructionException e) { + // Do not nest AutofuzzConstructionExceptions. + throw e; + } catch (AutofuzzInvocationException e) { + // If an invocation fails while creating the arguments for another invocation, the exception + // should not be reported, so we rewrap it. + throw new AutofuzzConstructionException(e.getCause()); + } catch (Throwable t) { + throw new AutofuzzConstructionException(t); + } + } + + private Object consumeChecked(FuzzedDataProvider data, Class<?>[] types, int i) { + if (types[i] == Unknown.class) { + throw new AutofuzzError("Failed to determine type of argument " + (i + 1)); + } + Object result; + try { + result = consumeNonStatic(data, types[i]); + } catch (AutofuzzConstructionException e) { + // Do not nest AutofuzzConstructionExceptions. + throw e; + } catch (AutofuzzInvocationException e) { + // If an invocation fails while creating the arguments for another invocation, the exception + // should not be reported, so we rewrap it. + throw new AutofuzzConstructionException(e.getCause()); + } catch (Throwable t) { + throw new AutofuzzConstructionException(t); + } + if (result != null && !types[i].isAssignableFrom(result.getClass())) { + throw new AutofuzzError("consume returned " + result.getClass() + ", but need " + types[i]); + } + return result; + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/autofuzz/YourAverageJavaClass.java b/src/main/java/com/code_intelligence/jazzer/autofuzz/YourAverageJavaClass.java new file mode 100644 index 00000000..452ca878 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/autofuzz/YourAverageJavaClass.java @@ -0,0 +1,229 @@ +// Copyright 2021 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 java.io.Closeable; +import java.io.IOException; +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +// Returned by Meta when asked to construct a Class object. Its purpose is to be a relatively +// "interesting" Java data class that can serve as the target of methods that perform some kind of +// reflection or deserialization. +public class YourAverageJavaClass implements Cloneable, Closeable, Serializable { + public byte aByte; + public boolean aBoolean; + public double aDouble; + public float aFloat; + public int anInt; + public transient int transientInt; + public long aLong; + public short aShort; + public volatile short volatileShort; + public String string; + public byte[] bytes; + public List<YourAverageJavaClass> list; + public Map<String, YourAverageJavaClass> map; + + // Everything below has been automatically generated (apart from a minor modification to clone()); + + public YourAverageJavaClass(byte aByte, boolean aBoolean, double aDouble, float aFloat, int anInt, + int transientInt, long aLong, short aShort, short volatileShort, String string) { + this.aByte = aByte; + this.aBoolean = aBoolean; + this.aDouble = aDouble; + this.aFloat = aFloat; + this.anInt = anInt; + this.transientInt = transientInt; + this.aLong = aLong; + this.aShort = aShort; + this.volatileShort = volatileShort; + this.string = string; + } + + public YourAverageJavaClass() {} + + public YourAverageJavaClass(byte aByte, boolean aBoolean, double aDouble, float aFloat, int anInt, + int transientInt, long aLong, short aShort, short volatileShort, String string, byte[] bytes, + List<YourAverageJavaClass> list, Map<String, YourAverageJavaClass> map) { + this.aByte = aByte; + this.aBoolean = aBoolean; + this.aDouble = aDouble; + this.aFloat = aFloat; + this.anInt = anInt; + this.transientInt = transientInt; + this.aLong = aLong; + this.aShort = aShort; + this.volatileShort = volatileShort; + this.string = string; + this.bytes = bytes; + this.list = list; + this.map = map; + } + + public byte getaByte() { + return aByte; + } + + public void setaByte(byte aByte) { + this.aByte = aByte; + } + + public boolean isaBoolean() { + return aBoolean; + } + + public void setaBoolean(boolean aBoolean) { + this.aBoolean = aBoolean; + } + + public double getaDouble() { + return aDouble; + } + + public void setaDouble(double aDouble) { + this.aDouble = aDouble; + } + + public float getaFloat() { + return aFloat; + } + + public void setaFloat(float aFloat) { + this.aFloat = aFloat; + } + + public int getAnInt() { + return anInt; + } + + public void setAnInt(int anInt) { + this.anInt = anInt; + } + + public int getTransientInt() { + return transientInt; + } + + public void setTransientInt(int transientInt) { + this.transientInt = transientInt; + } + + public long getaLong() { + return aLong; + } + + public void setaLong(long aLong) { + this.aLong = aLong; + } + + public short getaShort() { + return aShort; + } + + public void setaShort(short aShort) { + this.aShort = aShort; + } + + public short getVolatileShort() { + return volatileShort; + } + + public void setVolatileShort(short volatileShort) { + this.volatileShort = volatileShort; + } + + public String getString() { + return string; + } + + public void setString(String string) { + this.string = string; + } + + public byte[] getBytes() { + return bytes; + } + + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + + public List<YourAverageJavaClass> getList() { + return list; + } + + public void setList(List<YourAverageJavaClass> list) { + this.list = list; + } + + public Map<String, YourAverageJavaClass> getMap() { + return map; + } + + public void setMap(Map<String, YourAverageJavaClass> map) { + this.map = map; + } + + @Override + public YourAverageJavaClass clone() { + try { + YourAverageJavaClass clone = (YourAverageJavaClass) super.clone(); + clone.transientInt = transientInt + 1; + clone.volatileShort = (short) (volatileShort - 1); + return clone; + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof YourAverageJavaClass)) + return false; + YourAverageJavaClass that = (YourAverageJavaClass) o; + return aByte == that.aByte && aBoolean == that.aBoolean + && Double.compare(that.aDouble, aDouble) == 0 && Float.compare(that.aFloat, aFloat) == 0 + && anInt == that.anInt && transientInt == that.transientInt && aLong == that.aLong + && aShort == that.aShort && volatileShort == that.volatileShort + && Objects.equals(string, that.string) && Arrays.equals(bytes, that.bytes) + && Objects.equals(list, that.list) && Objects.equals(map, that.map); + } + + @Override + public int hashCode() { + int result = Objects.hash(aByte, aBoolean, aDouble, aFloat, anInt, transientInt, aLong, aShort, + volatileShort, string, list, map); + result = 31 * result + Arrays.hashCode(bytes); + return result; + } + + @Override + public String toString() { + return "YourAverageJavaClass{" + + "aByte=" + aByte + ", aBoolean=" + aBoolean + ", aDouble=" + aDouble + ", aFloat=" + + aFloat + ", anInt=" + anInt + ", transientInt=" + transientInt + ", aLong=" + aLong + + ", aShort=" + aShort + ", volatileShort=" + volatileShort + ", string='" + string + '\'' + + ", bytes=" + Arrays.toString(bytes) + ", list=" + list + ", map=" + map + '}'; + } + + @Override + public void close() throws IOException {} +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel new file mode 100644 index 00000000..37202c6d --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel @@ -0,0 +1,177 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library") +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") +load("//bazel:kotlin.bzl", "ktlint") + +java_library( + name = "constants", + srcs = ["Constants.java"], + visibility = ["//src/main/java/com/code_intelligence/jazzer/driver:__subpackages__"], +) + +java_library( + name = "driver", + srcs = ["Driver.java"], + visibility = [ + "//src/main/java/com/code_intelligence/jazzer:__pkg__", + ], + deps = [ + ":fuzz_target_finder", + ":fuzz_target_holder", + ":fuzz_target_runner", + ":offline_instrumentor", + ":opt", + "//src/main/java/com/code_intelligence/jazzer/agent:agent_installer", + "//src/main/java/com/code_intelligence/jazzer/android:android_runtime", + "//src/main/java/com/code_intelligence/jazzer/driver/junit:junit_runner", + "//src/main/java/com/code_intelligence/jazzer/runtime:constants", + "//src/main/java/com/code_intelligence/jazzer/utils:log", + ], +) + +java_library( + name = "offline_instrumentor", + srcs = ["OfflineInstrumentor.java"], + visibility = [ + "//src/main/java/com/code_intelligence/jazzer:__pkg__", + ], + deps = [ + ":opt", + "//src/main/java/com/code_intelligence/jazzer/agent:agent_installer", + "//src/main/java/com/code_intelligence/jazzer/utils:log", + "//src/main/java/com/code_intelligence/jazzer/utils:zip_utils", + ], +) + +kt_jvm_library( + name = "exception_utils", + srcs = ["ExceptionUtils.kt"], + visibility = ["//src/main/java/com/code_intelligence/jazzer/driver:__subpackages__"], + deps = [ + ":opt", + "//src/main/java/com/code_intelligence/jazzer/api:hooks", + "//src/main/java/com/code_intelligence/jazzer/runtime:constants", + "//src/main/java/com/code_intelligence/jazzer/utils:log", + ], +) + +java_library( + name = "fuzz_target_finder", + srcs = ["FuzzTargetFinder.java"], + visibility = ["//src/test/java/com/code_intelligence/jazzer/driver:__pkg__"], + deps = [ + ":fuzz_target_holder", + ":opt", + "//src/main/java/com/code_intelligence/jazzer/api", + "//src/main/java/com/code_intelligence/jazzer/runtime:constants", + "//src/main/java/com/code_intelligence/jazzer/utils:log", + "//src/main/java/com/code_intelligence/jazzer/utils:manifest_utils", + ], +) + +java_library( + name = "fuzz_target_holder", + srcs = ["FuzzTargetHolder.java"], + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/junit:__pkg__", + "//src/test/java/com/code_intelligence/jazzer/driver:__pkg__", + ], + deps = [ + ":opt", + "//src/main/java/com/code_intelligence/jazzer/api", + "//src/main/java/com/code_intelligence/jazzer/autofuzz", + ], +) + +java_jni_library( + name = "fuzz_target_runner", + srcs = ["FuzzTargetRunner.java"], + # This library is loaded by the classes in the agent runtime package as it needs to be available + # in the bootstrap class loader. It is packaged here rather than in jazzer_boostrap.jar since + # the bootstrap class loader doesn't support resources. + native_libs = [ + "//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver", + ], + visibility = [ + "//examples/junit/src/test/java/com/example:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/driver/junit:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/junit:__pkg__", + "//src/test:__subpackages__", + ], + deps = [ + ":constants", + ":exception_utils", + ":fuzz_target_holder", + ":fuzzed_data_provider_impl", + ":opt", + ":recording_fuzzed_data_provider", + ":reproducer_template", + ":signal_handler", + "//src/main/java/com/code_intelligence/jazzer/api", + "//src/main/java/com/code_intelligence/jazzer/autofuzz", + "//src/main/java/com/code_intelligence/jazzer/instrumentor", + "//src/main/java/com/code_intelligence/jazzer/mutation", + "//src/main/java/com/code_intelligence/jazzer/runtime:constants", + "//src/main/java/com/code_intelligence/jazzer/runtime:jazzer_bootstrap_compile_only", + "//src/main/java/com/code_intelligence/jazzer/utils:log", + "//src/main/java/com/code_intelligence/jazzer/utils:manifest_utils", + "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider", + ], +) + +java_jni_library( + name = "fuzzed_data_provider_impl", + srcs = ["FuzzedDataProviderImpl.java"], + native_libs = ["//src/main/native/com/code_intelligence/jazzer/driver:jazzer_fuzzed_data_provider"], + visibility = [ + "//src:__subpackages__", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/api", + "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider", + ], +) + +java_library( + name = "reproducer_template", + srcs = ["ReproducerTemplate.java"], + resources = ["Reproducer.java.tmpl"], + deps = [ + ":opt", + "//src/main/java/com/code_intelligence/jazzer/utils:log", + ], +) + +java_library( + name = "opt", + srcs = [ + "Opt.java", + "OptParser.java", + ], + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/driver:__subpackages__", + "//src/main/java/com/code_intelligence/jazzer/junit:__pkg__", + "//src/test/java/com/code_intelligence/jazzer/driver:__subpackages__", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer:constants", + "//src/main/java/com/code_intelligence/jazzer/utils:log", + ], +) + +java_library( + name = "recording_fuzzed_data_provider", + srcs = ["RecordingFuzzedDataProvider.java"], + visibility = ["//src/test/java/com/code_intelligence/jazzer/driver:__pkg__"], + deps = ["//src/main/java/com/code_intelligence/jazzer/api"], +) + +java_jni_library( + name = "signal_handler", + srcs = ["SignalHandler.java"], + native_libs = ["//src/main/native/com/code_intelligence/jazzer/driver:jazzer_signal_handler"], + visibility = ["//src/main/native/com/code_intelligence/jazzer/driver:__pkg__"], + deps = [":opt"], +) + +ktlint() diff --git a/src/main/java/com/code_intelligence/jazzer/driver/Constants.java b/src/main/java/com/code_intelligence/jazzer/driver/Constants.java new file mode 100644 index 00000000..80d476d8 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/Constants.java @@ -0,0 +1,24 @@ +/* + * 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.driver; + +public final class Constants { + // Default value of the libFuzzer -error_exitcode flag. + public static final int JAZZER_FINDING_EXIT_CODE = 77; + + private Constants(){}; +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/Driver.java b/src/main/java/com/code_intelligence/jazzer/driver/Driver.java new file mode 100644 index 00000000..8640e6c3 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/Driver.java @@ -0,0 +1,161 @@ +/* + * Copyright 2022 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.driver; + +import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID; +import static java.lang.System.exit; + +import com.code_intelligence.jazzer.agent.AgentInstaller; +import com.code_intelligence.jazzer.driver.junit.JUnitRunner; +import com.code_intelligence.jazzer.utils.Log; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.SecureRandom; +import java.util.List; +import java.util.Optional; + +public class Driver { + public static int start(List<String> args, boolean spawnsSubprocesses) throws IOException { + if (IS_ANDROID) { + if (!System.getProperty("jazzer.autofuzz", "").isEmpty()) { + Log.error("--autofuzz is not supported for Android"); + return 1; + } + if (!System.getProperty("jazzer.coverage_report", "").isEmpty()) { + Log.warn("--coverage_report is not supported for Android and has been disabled"); + System.clearProperty("jazzer.coverage_report"); + } + if (!System.getProperty("jazzer.coverage_dump", "").isEmpty()) { + Log.warn("--coverage_dump is not supported for Android and has been disabled"); + System.clearProperty("jazzer.coverage_dump"); + } + } + + if (spawnsSubprocesses) { + if (!System.getProperty("jazzer.coverage_report", "").isEmpty()) { + Log.warn("--coverage_report does not support parallel fuzzing and has been disabled"); + System.clearProperty("jazzer.coverage_report"); + } + if (!System.getProperty("jazzer.coverage_dump", "").isEmpty()) { + Log.warn("--coverage_dump does not support parallel fuzzing and has been disabled"); + System.clearProperty("jazzer.coverage_dump"); + } + + String idSyncFileArg = System.getProperty("jazzer.id_sync_file", ""); + Path idSyncFile; + if (idSyncFileArg.isEmpty()) { + // Create an empty temporary file used for coverage ID synchronization and + // pass its path to the agent in every child process. This requires adding + // the argument to argv for it to be picked up by libFuzzer, which then + // forwards it to child processes. + if (!IS_ANDROID) { + idSyncFile = Files.createTempFile("jazzer-", ""); + } else { + File f = File.createTempFile("jazzer-", "", new File("/data/local/tmp/")); + idSyncFile = f.toPath(); + } + + args.add("--id_sync_file=" + idSyncFile.toAbsolutePath()); + } else { + // Creates the file, truncating it if it exists. + idSyncFile = Files.write(Paths.get(idSyncFileArg), new byte[] {}); + } + // This wouldn't run in case we exit the process with _Exit, but the parent process of a -fork + // run is expected to exit with a regular exit(0), which does cause JVM shutdown hooks to run: + // https://github.com/llvm/llvm-project/blob/940e178c0018b32af2f1478d331fc41a92a7dac7/compiler-rt/lib/fuzzer/FuzzerFork.cpp#L491 + idSyncFile.toFile().deleteOnExit(); + } + + if (args.stream().anyMatch("-merge_inner=1" ::equals)) { + System.setProperty("jazzer.internal.merge_inner", "true"); + } + + // Jazzer's hooks use deterministic randomness and thus require a seed. Search for the last + // occurrence of a "-seed" argument as that is the one that is used by libFuzzer. If none is + // set, generate one and pass it to libFuzzer so that a fuzzing run can be reproduced simply by + // setting the seed printed by libFuzzer. + String seed = args.stream().reduce( + null, (prev, cur) -> cur.startsWith("-seed=") ? cur.substring("-seed=".length()) : prev); + if (seed == null) { + seed = Integer.toUnsignedString(new SecureRandom().nextInt()); + // Only add the -seed argument to the command line if not running in a mode + // that spawns subprocesses. These would inherit the same seed, which might + // make them less effective. + if (!spawnsSubprocesses) { + args.add("-seed=" + seed); + } + } + System.setProperty("jazzer.internal.seed", seed); + + if (args.stream().noneMatch(arg -> arg.startsWith("-rss_limit_mb="))) { + args.add(getDefaultRssLimitMbArg()); + } + + // Do not modify properties beyond this point, loading Opt locks in their values. + if (!Opt.instrumentOnly.isEmpty()) { + boolean instrumentationSuccess = OfflineInstrumentor.instrumentJars(Opt.instrumentOnly); + if (!instrumentationSuccess) { + exit(1); + } + exit(0); + } + + Driver.class.getClassLoader().setDefaultAssertionStatus(true); + + if (!Opt.autofuzz.isEmpty()) { + AgentInstaller.install(Opt.hooks); + FuzzTargetHolder.fuzzTarget = FuzzTargetHolder.AUTOFUZZ_FUZZ_TARGET; + return FuzzTargetRunner.startLibFuzzer(args); + } + + String targetClassName = FuzzTargetFinder.findFuzzTargetClassName(); + if (targetClassName == null) { + Log.error("Missing argument --target_class=<fuzz_target_class>"); + exit(1); + } + + // The JUnitRunner calls AgentInstaller.install itself after modifying flags affecting the + // agent. + if (JUnitRunner.isSupported()) { + Optional<JUnitRunner> runner = JUnitRunner.create(targetClassName, args); + if (runner.isPresent()) { + return runner.get().run(); + } + } + + // Installing the agent after the following "findFuzzTarget" leads to an asan error + // in it on "Class.forName(targetClassName)", but only during native fuzzing. + AgentInstaller.install(Opt.hooks); + FuzzTargetHolder.fuzzTarget = FuzzTargetFinder.findFuzzTarget(targetClassName); + return FuzzTargetRunner.startLibFuzzer(args); + } + + private static String getDefaultRssLimitMbArg() { + // Java OutOfMemoryErrors are strictly more informative than libFuzzer's out of memory crashes. + // We thus want to scale the default libFuzzer memory limit, which includes all memory used by + // the process including Jazzer's native and non-native memory footprint, such that: + // 1. we never reach it purely by allocating memory on the Java heap; + // 2. it is still reached if the fuzz target allocates excessively on the native heap. + // As a heuristic, we set the overall memory limit to 2 * the maximum size of the Java heap and + // add a fixed 1 GiB on top for the fuzzer's own memory usage. + long maxHeapInBytes = Runtime.getRuntime().maxMemory(); + return "-rss_limit_mb=" + ((2 * maxHeapInBytes / (1024 * 1024)) + 1024); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/ExceptionUtils.kt b/src/main/java/com/code_intelligence/jazzer/driver/ExceptionUtils.kt new file mode 100644 index 00000000..ed4b0569 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/ExceptionUtils.kt @@ -0,0 +1,215 @@ +// Copyright 2021 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. + +@file:JvmName("ExceptionUtils") + +package com.code_intelligence.jazzer.driver + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow +import com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID +import com.code_intelligence.jazzer.utils.Log +import java.lang.management.ManagementFactory +import java.nio.ByteBuffer +import java.security.MessageDigest + +private val JAZZER_PACKAGE_PREFIX = "com.code_intelligence.jazzer." +private val PUBLIC_JAZZER_PACKAGES = setOf("api", "replay", "sanitizers") + +private val StackTraceElement.isInternalFrame: Boolean + get() = if (!className.startsWith(JAZZER_PACKAGE_PREFIX)) { + false + } else { + val jazzerSubPackage = + className.substring(JAZZER_PACKAGE_PREFIX.length).split(".", limit = 2)[0] + jazzerSubPackage !in PUBLIC_JAZZER_PACKAGES + } + +private fun hash(throwable: Throwable, passToRootCause: Boolean): ByteArray = + MessageDigest.getInstance("SHA-256").run { + // It suffices to hash the stack trace of the deepest cause as the higher-level causes only + // contain part of the stack trace (plus possibly a different exception type). + var rootCause = throwable + if (passToRootCause) { + while (true) { + rootCause = rootCause.cause ?: break + } + } + update(rootCause.javaClass.name.toByteArray()) + rootCause.stackTrace + .takeWhile { !it.isInternalFrame } + .filterNot { + it.className.startsWith("jdk.internal.") || + it.className.startsWith("java.lang.reflect.") || + it.className.startsWith("sun.reflect.") || + it.className.startsWith("java.lang.invoke.") + } + .forEach { update(it.toString().toByteArray()) } + if (throwable.suppressed.isNotEmpty()) { + update("suppressed".toByteArray()) + for (suppressed in throwable.suppressed) { + update(hash(suppressed, passToRootCause)) + } + } + digest() + } + +/** + * Computes a hash of the stack trace of [throwable] without messages. + * + * The hash can be used to deduplicate stack traces obtained on crashes. By not including the + * messages, this hash should not depend on the precise crashing input. + */ +fun computeDedupToken(throwable: Throwable): Long { + var passToRootCause = true + if (throwable is FuzzerSecurityIssueLow && throwable.cause is StackOverflowError) { + // Special handling for StackOverflowErrors as processed by preprocessThrowable: + // Only consider the repeated part of the stack trace and ignore the original stack trace in + // the cause. + passToRootCause = false + } + return ByteBuffer.wrap(hash(throwable, passToRootCause)).long +} + +/** + * Annotates [throwable] with a severity and additional information if it represents a bug type + * that has security content. + */ +fun preprocessThrowable(throwable: Throwable): Throwable = when (throwable) { + is StackOverflowError -> { + // StackOverflowErrors are hard to deduplicate as the top-most stack frames vary wildly, + // whereas the information that is most useful for deduplication detection is hidden in the + // rest of the (truncated) stack frame. + // We heuristically clean up the stack trace by taking the elements from the bottom and + // stopping at the first repetition of a frame. The original error is returned as the cause + // unchanged. + val observedFrames = mutableSetOf<StackTraceElement>() + val bottomFramesWithoutRepetition = throwable.stackTrace.takeLastWhile { frame -> + (frame !in observedFrames).also { observedFrames.add(frame) } + } + var securityIssueMessage = "Stack overflow" + if (!IS_ANDROID) { + securityIssueMessage = "$securityIssueMessage (use '${getReproducingXssArg()}' to reproduce)" + } + FuzzerSecurityIssueLow(securityIssueMessage, throwable).apply { + stackTrace = bottomFramesWithoutRepetition.toTypedArray() + } + } + is OutOfMemoryError -> { + var securityIssueMessage = "Out of memory" + if (!IS_ANDROID) { + securityIssueMessage = "$securityIssueMessage (use '${getReproducingXmxArg()}' to reproduce)" + } + stripOwnStackTrace(FuzzerSecurityIssueLow(securityIssueMessage, throwable)) + } + is VirtualMachineError -> stripOwnStackTrace(FuzzerSecurityIssueLow(throwable)) + else -> throwable +}.also { dropInternalFrames(it) } + +/** + * Recursively strips all Jazzer-internal stack frames from the given [Throwable] and its causes. + */ +private fun dropInternalFrames(throwable: Throwable?) { + throwable?.run { + stackTrace = stackTrace.takeWhile { !it.isInternalFrame }.toTypedArray() + suppressed.forEach { it.stackTrace = stackTrace.takeWhile { !it.isInternalFrame }.toTypedArray() } + dropInternalFrames(throwable.cause) + } +} + +/** + * Strips the stack trace of [throwable] (e.g. because it was created in a utility method), but not + * the stack traces of its causes. + */ +private fun stripOwnStackTrace(throwable: Throwable) = throwable.apply { + stackTrace = emptyArray() +} + +/** + * Returns a valid `-Xmx` JVM argument that sets the stack size to a value with which [StackOverflowError] findings can + * be reproduced, assuming the environment is sufficiently similar (e.g. OS and JVM version). + */ +private fun getReproducingXmxArg(): String? { + val maxHeapSizeInMegaBytes = (getNumericFinalFlagValue("MaxHeapSize") ?: return null) shr 20 + val conservativeMaxHeapSizeInMegaBytes = (maxHeapSizeInMegaBytes * 0.9).toInt() + return "-Xmx${conservativeMaxHeapSizeInMegaBytes}m" +} + +/** + * Returns a valid `-Xss` JVM argument that sets the stack size to a value with which [StackOverflowError] findings can + * be reproduced, assuming the environment is sufficiently similar (e.g. OS and JVM version). + */ +private fun getReproducingXssArg(): String? { + val threadStackSizeInKiloBytes = getNumericFinalFlagValue("ThreadStackSize") ?: return null + val conservativeThreadStackSizeInKiloBytes = (threadStackSizeInKiloBytes * 0.9).toInt() + return "-Xss${conservativeThreadStackSizeInKiloBytes}k" +} + +private fun getNumericFinalFlagValue(arg: String): Long? { + val argPattern = "$arg\\D*(\\d*)".toRegex() + return argPattern.find(javaFullFinalFlags ?: return null)?.groupValues?.get(1)?.toLongOrNull() +} + +private val javaFullFinalFlags by lazy { + readJavaFullFinalFlags() +} + +private fun readJavaFullFinalFlags(): String? { + val javaHome = System.getProperty("java.home") ?: return null + val javaBinary = "$javaHome/bin/java" + val currentJvmArgs = ManagementFactory.getRuntimeMXBean().inputArguments + val javaPrintFlagsProcess = ProcessBuilder( + listOf(javaBinary) + currentJvmArgs + listOf( + "-XX:+PrintFlagsFinal", + "-version", + ), + ).start() + return javaPrintFlagsProcess.inputStream.bufferedReader().useLines { lineSequence -> + lineSequence + .filter { it.contains("ThreadStackSize") || it.contains("MaxHeapSize") } + .joinToString("\n") + } +} + +fun dumpAllStackTraces() { + Log.println("\nStack traces of all JVM threads:") + for ((thread, stack) in Thread.getAllStackTraces()) { + Log.println(thread.toString()) + // Remove traces of this method and the methods it calls. + stack.asList() + .asReversed() + .takeWhile { + !( + it.className == "com.code_intelligence.jazzer.driver.ExceptionUtils" && + it.methodName == "dumpAllStackTraces" + ) + } + .asReversed() + .forEach { frame -> + Log.println("\tat $frame") + } + Log.println("") + } + + if (IS_ANDROID) { + // ManagementFactory is not supported on Android + return + } + + Log.println("Garbage collector stats:") + Log.println( + ManagementFactory.getGarbageCollectorMXBeans().joinToString("\n", "\n", "\n") { + "${it.name}: ${it.collectionCount} collections took ${it.collectionTime}ms" + }, + ) +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetFinder.java b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetFinder.java new file mode 100644 index 00000000..c2c41774 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetFinder.java @@ -0,0 +1,137 @@ +/* + * Copyright 2022 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.driver; + +import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID; +import static java.lang.System.exit; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.driver.FuzzTargetHolder.FuzzTarget; +import com.code_intelligence.jazzer.utils.Log; +import com.code_intelligence.jazzer.utils.ManifestUtils; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +class FuzzTargetFinder { + private static final String FUZZER_TEST_ONE_INPUT = "fuzzerTestOneInput"; + private static final String FUZZER_INITIALIZE = "fuzzerInitialize"; + private static final String FUZZER_TEAR_DOWN = "fuzzerTearDown"; + + static String findFuzzTargetClassName() { + if (!Opt.targetClass.isEmpty()) { + return Opt.targetClass; + } + if (IS_ANDROID) { + // Fuzz target detection tools aren't supported on android + return null; + } + return ManifestUtils.detectFuzzTargetClass(); + } + + /** + * @throws IllegalArgumentException if the fuzz target method is invalid or couldn't be found + * @param targetClassName name of the fuzz target class + * @return a {@link FuzzTarget} + */ + static FuzzTarget findFuzzTarget(String targetClassName) { + Class<?> fuzzTargetClass; + try { + fuzzTargetClass = + Class.forName(targetClassName, false, FuzzTargetFinder.class.getClassLoader()); + } catch (ClassNotFoundException e) { + Log.error(String.format( + "'%s' not found on classpath:%n%n%s%n%nAll required classes must be on the classpath specified via --cp.", + targetClassName, System.getProperty("java.class.path"))); + exit(1); + throw new IllegalStateException("Not reached"); + } + + return findFuzzTargetByMethodName(fuzzTargetClass); + } + + // Finds the traditional static fuzzerTestOneInput fuzz target method. + private static FuzzTarget findFuzzTargetByMethodName(Class<?> clazz) { + Method fuzzTargetMethod; + if (Opt.experimentalMutator) { + List<Method> fuzzTargetMethods = + Arrays.stream(clazz.getMethods()) + .filter(method -> "fuzzerTestOneInput".equals(method.getName())) + .filter(method -> Modifier.isStatic(method.getModifiers())) + .collect(Collectors.toList()); + if (fuzzTargetMethods.size() != 1) { + throw new IllegalArgumentException( + String.format("%s must define exactly one function of this form:%n" + + "public static void fuzzerTestOneInput(...)%n", + clazz.getName())); + } + fuzzTargetMethod = fuzzTargetMethods.get(0); + } else { + Optional<Method> bytesFuzzTarget = + targetPublicStaticMethod(clazz, FUZZER_TEST_ONE_INPUT, byte[].class); + Optional<Method> dataFuzzTarget = + targetPublicStaticMethod(clazz, FUZZER_TEST_ONE_INPUT, FuzzedDataProvider.class); + if (bytesFuzzTarget.isPresent() == dataFuzzTarget.isPresent()) { + throw new IllegalArgumentException(String.format( + "%s must define exactly one of the following two functions:%n" + + "public static void fuzzerTestOneInput(byte[] ...)%n" + + "public static void fuzzerTestOneInput(FuzzedDataProvider ...)%n" + + "Note: Fuzz targets returning boolean are no longer supported; exceptions should be thrown instead of returning true.", + clazz.getName())); + } + fuzzTargetMethod = dataFuzzTarget.orElseGet(bytesFuzzTarget::get); + } + + Callable<Object> initialize = + Stream + .of(targetPublicStaticMethod(clazz, FUZZER_INITIALIZE, String[].class) + .map(init -> (Callable<Object>) () -> { + init.invoke(null, (Object) Opt.targetArgs.toArray(new String[] {})); + return null; + }), + targetPublicStaticMethod(clazz, FUZZER_INITIALIZE) + .map(init -> (Callable<Object>) () -> { + init.invoke(null); + return null; + })) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .orElse(() -> null); + + return new FuzzTarget( + fuzzTargetMethod, initialize, targetPublicStaticMethod(clazz, FUZZER_TEAR_DOWN)); + } + + private static Optional<Method> targetPublicStaticMethod( + Class<?> clazz, String name, Class<?>... parameterTypes) { + try { + Method method = clazz.getMethod(name, parameterTypes); + if (!Modifier.isStatic(method.getModifiers()) || !Modifier.isPublic(method.getModifiers())) { + return Optional.empty(); + } + return Optional.of(method); + } catch (NoSuchMethodException e) { + return Optional.empty(); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetHolder.java b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetHolder.java new file mode 100644 index 00000000..e748c6f7 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetHolder.java @@ -0,0 +1,62 @@ +/* + * 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.driver; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.lang.reflect.Method; +import java.util.Optional; +import java.util.concurrent.Callable; + +public class FuzzTargetHolder { + public static FuzzTarget autofuzzFuzzTarget(Callable<Object> newInstance) { + try { + Method fuzzerTestOneInput = com.code_intelligence.jazzer.autofuzz.FuzzTarget.class.getMethod( + "fuzzerTestOneInput", FuzzedDataProvider.class); + return new FuzzTargetHolder.FuzzTarget(fuzzerTestOneInput, newInstance, Optional.empty()); + } catch (NoSuchMethodException e) { + throw new IllegalStateException(e); + } + } + + public static final FuzzTarget AUTOFUZZ_FUZZ_TARGET = autofuzzFuzzTarget(() -> { + com.code_intelligence.jazzer.autofuzz.FuzzTarget.fuzzerInitialize( + Opt.targetArgs.toArray(new String[0])); + return null; + }); + + /** + * The fuzz target that {@link FuzzTargetRunner} should fuzz. + */ + public static FuzzTarget fuzzTarget; + + public static class FuzzTarget { + public final Method method; + public final Callable<Object> newInstance; + public final Optional<Method> tearDown; + + public FuzzTarget(Method method, Callable<Object> newInstance, Optional<Method> tearDown) { + this.method = method; + this.newInstance = newInstance; + this.tearDown = tearDown; + } + + public boolean usesFuzzedDataProvider() { + return this.method.getParameterCount() == 1 + && this.method.getParameterTypes()[0] == FuzzedDataProvider.class; + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java new file mode 100644 index 00000000..aefa5352 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java @@ -0,0 +1,542 @@ +/* + * 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.driver; + +import static com.code_intelligence.jazzer.driver.Constants.JAZZER_FINDING_EXIT_CODE; +import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID; +import static java.lang.System.exit; +import static java.util.stream.Collectors.joining; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.autofuzz.FuzzTarget; +import com.code_intelligence.jazzer.instrumentor.CoverageRecorder; +import com.code_intelligence.jazzer.mutation.ArgumentsMutator; +import com.code_intelligence.jazzer.runtime.FuzzTargetRunnerNatives; +import com.code_intelligence.jazzer.runtime.JazzerInternal; +import com.code_intelligence.jazzer.utils.Log; +import com.code_intelligence.jazzer.utils.UnsafeProvider; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; +import sun.misc.Unsafe; + +/** + * Executes a fuzz target and reports findings. + * + * <p>This class maintains global state (both native and non-native) and thus cannot be used + * concurrently. + */ +public final class FuzzTargetRunner { + private static final String OPENTEST4J_TEST_ABORTED_EXCEPTION = + "org.opentest4j.TestAbortedException"; + + private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe(); + + private static final long BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class); + + // Possible return values for the libFuzzer callback runOne. + private static final int LIBFUZZER_CONTINUE = 0; + private static final int LIBFUZZER_RETURN_FROM_DRIVER = -2; + + private static boolean invalidCorpusFileWarningShown = false; + private static final Set<Long> ignoredTokens = new HashSet<>(Opt.ignore); + private static final FuzzedDataProviderImpl fuzzedDataProvider = + FuzzedDataProviderImpl.withNativeData(); + private static final MethodHandle fuzzTargetMethod; + private static final boolean useFuzzedDataProvider; + // Reused in every iteration analogous to JUnit's PER_CLASS lifecycle. + private static final Object fuzzTargetInstance; + private static final Method fuzzerTearDown; + private static final ArgumentsMutator mutator; + private static final ReproducerTemplate reproducerTemplate; + private static Predicate<Throwable> findingHandler; + + static { + FuzzTargetHolder.FuzzTarget fuzzTarget = FuzzTargetHolder.fuzzTarget; + Class<?> fuzzTargetClass = fuzzTarget.method.getDeclaringClass(); + + // The method may not be accessible - JUnit test classes and methods are usually declared + // without access modifiers and thus package-private. + fuzzTarget.method.setAccessible(true); + try { + fuzzTargetMethod = MethodHandles.lookup().unreflect(fuzzTarget.method); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + useFuzzedDataProvider = fuzzTarget.usesFuzzedDataProvider(); + if (!useFuzzedDataProvider && IS_ANDROID) { + Log.error("Android fuzz targets must use " + FuzzedDataProvider.class.getName()); + exit(1); + throw new IllegalStateException("Not reached"); + } + + fuzzerTearDown = fuzzTarget.tearDown.orElse(null); + reproducerTemplate = new ReproducerTemplate(fuzzTargetClass.getName(), useFuzzedDataProvider); + + JazzerInternal.onFuzzTargetReady(fuzzTargetClass.getName()); + + try { + fuzzTargetInstance = fuzzTarget.newInstance.call(); + } catch (Throwable t) { + Log.finding(t); + exit(1); + throw new IllegalStateException("Not reached"); + } + + if (Opt.experimentalMutator) { + if (Modifier.isStatic(fuzzTarget.method.getModifiers())) { + mutator = ArgumentsMutator.forStaticMethodOrThrow(fuzzTarget.method); + } else { + mutator = ArgumentsMutator.forInstanceMethodOrThrow(fuzzTargetInstance, fuzzTarget.method); + } + Log.info("Using experimental mutator: " + mutator); + } else { + mutator = null; + } + + if (Opt.hooks) { + // libFuzzer will clear the coverage map after this method returns and keeps no record of the + // coverage accumulated so far (e.g. by static initializers). We record it here to keep it + // around for JaCoCo coverage reports. + CoverageRecorder.updateCoveredIdsWithCoverageMap(); + } + + Runtime.getRuntime().addShutdownHook(new Thread(FuzzTargetRunner::shutdown)); + } + + /** + * A test-only convenience wrapper around {@link #runOne(long, int)}. + */ + static int runOne(byte[] data) { + long dataPtr = UNSAFE.allocateMemory(data.length); + UNSAFE.copyMemory(data, BYTE_ARRAY_OFFSET, null, dataPtr, data.length); + try { + return runOne(dataPtr, data.length); + } finally { + UNSAFE.freeMemory(dataPtr); + } + } + + /** + * Executes the user-provided fuzz target once. + * + * @param dataPtr a native pointer to beginning of the input provided by the fuzzer for this + * execution + * @param dataLength length of the fuzzer input + * @return the value that the native LLVMFuzzerTestOneInput function should return. Currently, + * this is always 0. The function may exit the process instead of returning. + */ + private static int runOne(long dataPtr, int dataLength) { + Throwable finding = null; + byte[] data; + Object argument; + if (Opt.experimentalMutator) { + // TODO: Instead of copying the native data and then reading it in, consider the following + // optimizations if they turn out to be worthwhile in benchmarks: + // 1. Let libFuzzer pass in a null pointer if the byte array hasn't changed since the last + // call to our custom mutator and skip the read entirely. + // 2. Implement a InputStream backed by Unsafe to avoid the copyToArray overhead. + byte[] buf = copyToArray(dataPtr, dataLength); + boolean readExactly = mutator.read(new ByteArrayInputStream(buf)); + + // All inputs constructed by the mutator framework can be read exactly, existing corpus files + // may not be valid for the current fuzz target anymore, though. In this case, print a warning + // once. + if (!(invalidCorpusFileWarningShown || readExactly || isFixedLibFuzzerInput(buf))) { + invalidCorpusFileWarningShown = true; + Log.warn("Some files in the seed corpus do not match the fuzz target signature. " + + "This indicates that they were generated with a different signature and may cause issues reproducing previous findings."); + } + data = null; + argument = null; + } else if (useFuzzedDataProvider) { + fuzzedDataProvider.setNativeData(dataPtr, dataLength); + data = null; + argument = fuzzedDataProvider; + } else { + data = copyToArray(dataPtr, dataLength); + argument = data; + } + try { + if (Opt.experimentalMutator) { + // No need to detach as we are currently reading in the mutator state from bytes in every + // iteration. + mutator.invoke(false); + } else if (fuzzTargetInstance == null) { + fuzzTargetMethod.invoke(argument); + } else { + fuzzTargetMethod.invoke(fuzzTargetInstance, argument); + } + } catch (Throwable uncaughtFinding) { + finding = uncaughtFinding; + } + + // When using libFuzzer's -merge flag, only the coverage of the current input is relevant, not + // whether it is crashing. Since every crash would cause a restart of the process and thus the + // JVM, we can optimize this case by not crashing. + // + // Incidentally, this makes the behavior of fuzz targets relying on global states more + // consistent: Rather than resetting the global state after every crashing input and thus + // dependent on the particular ordering of the inputs, we never reset it. + if (Opt.mergeInner) { + return LIBFUZZER_CONTINUE; + } + + // Explicitly reported findings take precedence over uncaught exceptions. + if (JazzerInternal.lastFinding != null) { + finding = JazzerInternal.lastFinding; + JazzerInternal.lastFinding = null; + } + // Allow skipping invalid inputs in fuzz tests by using e.g. JUnit's assumeTrue. + if (finding == null || finding.getClass().getName().equals(OPENTEST4J_TEST_ABORTED_EXCEPTION)) { + return LIBFUZZER_CONTINUE; + } + if (Opt.hooks) { + finding = ExceptionUtils.preprocessThrowable(finding); + } + + long dedupToken = Opt.dedup ? ExceptionUtils.computeDedupToken(finding) : 0; + if (Opt.dedup && !ignoredTokens.add(dedupToken)) { + return LIBFUZZER_CONTINUE; + } + + if (findingHandler != null) { + // We still print the libFuzzer crashing input information, which also dumps the crashing + // input as a side effect. + printCrashingInput(); + if (findingHandler.test(finding)) { + return LIBFUZZER_CONTINUE; + } else { + return LIBFUZZER_RETURN_FROM_DRIVER; + } + } + + // The user-provided fuzz target method has returned. Any further exits are on us and should not + // result in a "fuzz target exited" warning being printed by libFuzzer. + temporarilyDisableLibfuzzerExitHook(); + + Log.finding(finding); + if (Opt.dedup) { + // Has to be printed to stdout as it is parsed by libFuzzer when minimizing a crash. It does + // not necessarily have to appear at the beginning of a line. + // https://github.com/llvm/llvm-project/blob/4c106c93eb68f8f9f201202677cd31e326c16823/compiler-rt/lib/fuzzer/FuzzerDriver.cpp#L342 + Log.structuredOutput(String.format(Locale.ROOT, "DEDUP_TOKEN: %016x", dedupToken)); + } + Log.println("== libFuzzer crashing input =="); + printCrashingInput(); + // dumpReproducer needs to be called after libFuzzer printed its final stats as otherwise it + // would report incorrect coverage - the reproducer generation involved rerunning the fuzz + // target. + // It doesn't support @FuzzTest fuzz targets, but these come with an integrated regression test + // that satisfies the same purpose. + // It also doesn't support the experimental mutator yet as that requires implementing Java code + // generation for mutators. + if (fuzzTargetInstance == null && !Opt.experimentalMutator) { + dumpReproducer(data); + } + + if (!Opt.dedup || Long.compareUnsigned(ignoredTokens.size(), Opt.keepGoing) >= 0) { + // Reached the maximum amount of findings to keep going for, crash after shutdown. We use + // _Exit rather than System.exit to not trigger libFuzzer's exit handlers. + if (!Opt.autofuzz.isEmpty() && Opt.dedup) { + Log.println(""); + Log.info(String.format( + "To continue fuzzing past this particular finding, rerun with the following additional argument:" + + "%n%n --ignore=%s%n%n" + + "To ignore all findings of this kind, rerun with the following additional argument:" + + "%n%n --autofuzz_ignore=%s", + ignoredTokens.stream() + .map(token -> Long.toUnsignedString(token, 16)) + .collect(joining(",")), + Stream.concat(Opt.autofuzzIgnore.stream(), Stream.of(finding.getClass().getName())) + .collect(joining(",")))); + } + System.exit(JAZZER_FINDING_EXIT_CODE); + throw new IllegalStateException("Not reached"); + } + return LIBFUZZER_CONTINUE; + } + + private static boolean isFixedLibFuzzerInput(byte[] input) { + // Detect special libFuzzer inputs which can not be processed by the mutator framework. + // libFuzzer always uses an empty input, and one with a single line feed (10) to indicate + // end of initial corpus file processing. + return input.length == 0 || (input.length == 1 && input[0] == 10); + } + + // Called via JNI, being passed data from LLVMFuzzerCustomMutator. + @SuppressWarnings("unused") + private static int mutateOne(long data, int size, int maxSize, int seed) { + mutate(data, size, seed); + return writeToMemory(mutator, data, maxSize); + } + + private static void mutate(long data, int size, int seed) { + // libFuzzer sends the input "\n" when there are no corpus entries. We use that as a signal to + // initialize the mutator instead of just reading that trivial input to produce a more + // interesting value. + if (size == 1 && UNSAFE.getByte(data) == '\n') { + mutator.init(seed); + } else { + // TODO: See the comment on earlier calls to read for potential optimizations. + mutator.read(new ByteArrayInputStream(copyToArray(data, size))); + mutator.mutate(seed); + } + } + + private static long crossOverCount = 0; + + // Called via JNI, being passed data from LLVMFuzzerCustomCrossOver. + @SuppressWarnings("unused") + private static int crossOver( + long data1, int size1, long data2, int size2, long out, int maxOutSize, int seed) { + // Custom cross over and custom mutate are the only mutators registered in + // libFuzzer, hence cross over is picked as often as mutate, which is way + // too frequently. Without custom mutate, cross over would be picked from + // the list of default mutators, so ~1/12 of the time. This also seems too + // much and is reduced to a configurable frequency, default 1/100, here, + // mutate is used in the other cases. + if (Opt.experimentalCrossOverFrequency != 0 + && crossOverCount++ % Opt.experimentalCrossOverFrequency == 0) { + mutator.crossOver(new ByteArrayInputStream(copyToArray(data1, size1)), + new ByteArrayInputStream(copyToArray(data2, size2)), seed); + } else { + mutate(data1, size1, seed); + } + return writeToMemory(mutator, out, maxOutSize); + } + + @SuppressWarnings("SameParameterValue") + private static int writeToMemory(ArgumentsMutator mutator, long out, int maxOutSize) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + // TODO: Instead of writing to a byte array and then copying that array's contents into + // memory, consider introducing an OutputStream backed by Unsafe. + mutator.write(baos); + byte[] mutatedBytes = baos.toByteArray(); + int newSize = Math.min(mutatedBytes.length, maxOutSize); + UNSAFE.copyMemory(mutatedBytes, BYTE_ARRAY_OFFSET, null, out, newSize); + return newSize; + } + + /* + * Starts libFuzzer via LLVMFuzzerRunDriver. + */ + public static int startLibFuzzer(List<String> args) { + // We always define LLVMFuzzerCustomMutator, but only use it when --experimental_mutator is + // specified. libFuzzer contains logic that disables --len_control when it finds the custom + // mutator symbol: + // https://github.com/llvm/llvm-project/blob/da3623de2411dd931913eb510e94fe846c929c24/compiler-rt/lib/fuzzer/FuzzerDriver.cpp#L202-L207 + // We thus have to explicitly set --len_control to its default value when not using the new + // mutator. + // TODO: libFuzzer still emits a message about --len_control being disabled by default even if + // we override it via a flag. We may want to patch this out. + if (!Opt.experimentalMutator) { + // args may not be mutable. + args = new ArrayList<>(args); + // https://github.com/llvm/llvm-project/blob/da3623de2411dd931913eb510e94fe846c929c24/compiler-rt/lib/fuzzer/FuzzerFlags.def#L19 + args.add("-len_control=100"); + } + + for (String arg : args.subList(1, args.size())) { + if (!arg.startsWith("-")) { + Log.info("using inputs from: " + arg); + } + } + + if (!IS_ANDROID) { + SignalHandler.initialize(); + } + return startLibFuzzer( + args.stream().map(str -> str.getBytes(StandardCharsets.UTF_8)).toArray(byte[][] ::new)); + } + + /** + * Registers a custom handler for findings. + * + * @param findingHandler a consumer for the finding that returns true if the fuzzer should + * continue fuzzing and false if it should return from + * {@link FuzzTargetRunner#startLibFuzzer(List)}. + */ + public static void registerFindingHandler(Predicate<Throwable> findingHandler) { + FuzzTargetRunner.findingHandler = findingHandler; + } + + private static void shutdown() { + if (!Opt.coverageDump.isEmpty() || !Opt.coverageReport.isEmpty()) { + if (!Opt.coverageDump.isEmpty()) { + CoverageRecorder.dumpJacocoCoverage(Opt.coverageDump); + } + if (!Opt.coverageReport.isEmpty()) { + CoverageRecorder.dumpCoverageReport(Opt.coverageReport); + } + } + + if (fuzzerTearDown == null) { + return; + } + Log.info("calling fuzzerTearDown function"); + try { + fuzzerTearDown.invoke(null); + } catch (InvocationTargetException e) { + Log.finding(e.getCause()); + System.exit(JAZZER_FINDING_EXIT_CODE); + } catch (Throwable t) { + Log.error(t); + System.exit(1); + } + } + + private static void dumpReproducer(byte[] data) { + if (data == null) { + assert useFuzzedDataProvider; + fuzzedDataProvider.reset(); + data = fuzzedDataProvider.consumeRemainingAsBytes(); + } + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-1 not available", e); + } + String dataSha1 = toHexString(digest.digest(data)); + + if (!Opt.autofuzz.isEmpty()) { + fuzzedDataProvider.reset(); + FuzzTarget.dumpReproducer(fuzzedDataProvider, Opt.reproducerPath, dataSha1); + return; + } + + String base64Data; + if (useFuzzedDataProvider) { + fuzzedDataProvider.reset(); + FuzzedDataProvider recordingFuzzedDataProvider = + RecordingFuzzedDataProvider.makeFuzzedDataProviderProxy(fuzzedDataProvider); + try { + fuzzTargetMethod.invokeExact(recordingFuzzedDataProvider); + if (JazzerInternal.lastFinding == null) { + Log.warn("Failed to reproduce crash when rerunning with recorder"); + } + } catch (Throwable ignored) { + // Expected. + } + try { + base64Data = RecordingFuzzedDataProvider.serializeFuzzedDataProviderProxy( + recordingFuzzedDataProvider); + } catch (IOException e) { + Log.error("Failed to create reproducer", e); + // Don't let libFuzzer print a native stack trace. + System.exit(1); + throw new IllegalStateException("Not reached"); + } + } else { + base64Data = Base64.getEncoder().encodeToString(data); + } + + reproducerTemplate.dumpReproducer(base64Data, dataSha1); + } + + /** + * Convert a byte array to a lower-case hex string. + * + * <p>The returned hex string always has {@code 2 * bytes.length} characters. + * + * @param bytes the bytes to convert + * @return a lower-case hex string representing the bytes + */ + private static String toHexString(byte[] bytes) { + String unpadded = new BigInteger(1, bytes).toString(16); + int numLeadingZeroes = 2 * bytes.length - unpadded.length(); + return String.join("", Collections.nCopies(numLeadingZeroes, "0")) + unpadded; + } + + // Accessed by fuzz_target_runner.cpp. + @SuppressWarnings("unused") + private static void dumpAllStackTraces() { + ExceptionUtils.dumpAllStackTraces(); + } + + private static byte[] copyToArray(long ptr, int length) { + // TODO: Use Unsafe.allocateUninitializedArray instead once Java 9 is the base. + byte[] array = new byte[length]; + UNSAFE.copyMemory(null, ptr, array, BYTE_ARRAY_OFFSET, length); + return array; + } + + /** + * Starts libFuzzer via LLVMFuzzerRunDriver. + * + * @param args command-line arguments encoded in UTF-8 (not null-terminated) + * @return the return value of LLVMFuzzerRunDriver + */ + private static int startLibFuzzer(byte[][] args) { + return FuzzTargetRunnerNatives.startLibFuzzer( + args, FuzzTargetRunner.class, Opt.experimentalMutator); + } + + /** + * Causes libFuzzer to write the current input to disk as a crashing input and emit some + * information about it to stderr. + */ + public static void printCrashingInput() { + FuzzTargetRunnerNatives.printCrashingInput(); + } + + /** + * Returns the debug string of the current mutator. + * If no mutator is used, returns null. + */ + public static String mutatorDebugString() { + return mutator != null ? mutator.toString() : null; + } + + /** + * Returns whether the current mutator has detected invalid corpus files. + * If no mutator is used, returns false. + */ + public static boolean invalidCorpusFilesPresent() { + return mutator != null && invalidCorpusFileWarningShown; + } + + /** + * Disables libFuzzer's fuzz target exit detection until the next call to {@link #runOne}. + * + * <p>Calling {@link System#exit} after having called this method will not trigger libFuzzer's + * exit hook that would otherwise print the "fuzz target exited" error message. This method should + * thus only be called after control has returned from the user-provided fuzz target. + */ + private static void temporarilyDisableLibfuzzerExitHook() { + FuzzTargetRunnerNatives.temporarilyDisableLibfuzzerExitHook(); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/FuzzedDataProviderImpl.java b/src/main/java/com/code_intelligence/jazzer/driver/FuzzedDataProviderImpl.java new file mode 100644 index 00000000..08e5298b --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/FuzzedDataProviderImpl.java @@ -0,0 +1,257 @@ +// Copyright 2021 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.driver; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.utils.UnsafeProvider; +import com.github.fmeum.rules_jni.RulesJni; +import sun.misc.Unsafe; + +public class FuzzedDataProviderImpl implements FuzzedDataProvider, AutoCloseable { + static { + RulesJni.loadLibrary("jazzer_fuzzed_data_provider", "/com/code_intelligence/jazzer/driver"); + nativeInit(); + } + + private static native void nativeInit(); + + private final byte[] javaData; + private long originalDataPtr; + private int originalRemainingBytes; + + // Accessed in fuzzed_data_provider.cpp. + private long dataPtr; + private int remainingBytes; + + private FuzzedDataProviderImpl(long dataPtr, int remainingBytes, byte[] javaData) { + this.javaData = javaData; + this.originalDataPtr = dataPtr; + this.dataPtr = dataPtr; + this.originalRemainingBytes = remainingBytes; + this.remainingBytes = remainingBytes; + } + + /** + * Creates a {@link FuzzedDataProvider} that consumes bytes from an already existing native array. + * + * <ul> + * <li>{@link #close()} <b>must</b> be called on instances created with this method to free the + * native copy of the Java + * {@code byte} array. + * <li>{@link #setNativeData(long, int)} <b>must not</b> be called on instances created with this + * method. + * + * @param data the raw bytes used as input + * @return a {@link FuzzedDataProvider} backed by {@code data} + */ + public static FuzzedDataProviderImpl withJavaData(byte[] data) { + return new FuzzedDataProviderImpl(allocateNativeCopy(data), data.length, data); + } + + /** + * Creates a {@link FuzzedDataProvider} that consumes bytes from an already existing native array. + * + * <p>The backing array can be set at any time using {@link #setNativeData(long, int)} and is + * initially empty. + * + * @return a {@link FuzzedDataProvider} backed by an empty array. + */ + public static FuzzedDataProviderImpl withNativeData() { + return new FuzzedDataProviderImpl(0, 0, null); + } + + /** + * Replaces the current native backing array. + * + * <p><b>Must not</b> be called on instances created with {@link #withJavaData(byte[])}. + * + * @param dataPtr a native pointer to the new backing array + * @param dataLength the length of the new backing array + */ + public void setNativeData(long dataPtr, int dataLength) { + this.originalDataPtr = dataPtr; + this.dataPtr = dataPtr; + this.originalRemainingBytes = dataLength; + this.remainingBytes = dataLength; + } + + /** + * Returns the Java byte array used to construct the instance, or null if it was created with + * {@link FuzzedDataProviderImpl#withNativeData()}; + */ + public byte[] getJavaData() { + return javaData; + } + + /** + * Resets the FuzzedDataProvider state to read from the beginning to the end of its current + * backing item. + */ + public void reset() { + dataPtr = originalDataPtr; + remainingBytes = originalRemainingBytes; + } + + /** + * Releases native memory allocated for this instance (if any). + * + * <p>While the instance should not be used after this method returns, no usage of {@link + * FuzzedDataProvider} methods can result in memory corruption. + */ + @Override + public void close() { + if (originalDataPtr == 0) { + return; + } + // We own the native memory iff the instance was created backed by a Java byte array. + if (javaData != null) { + UNSAFE.freeMemory(originalDataPtr); + } + // Prevent double-frees and use-after-frees by effectively making all methods no-ops after + // close() has been called. + originalDataPtr = 0; + originalRemainingBytes = 0; + dataPtr = 0; + remainingBytes = 0; + } + + private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe(); + private static final long BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class); + + private static long allocateNativeCopy(byte[] data) { + long nativeCopy = UNSAFE.allocateMemory(data.length); + UNSAFE.copyMemory(data, BYTE_ARRAY_OFFSET, null, nativeCopy, data.length); + return nativeCopy; + } + + @Override public native boolean consumeBoolean(); + + @Override public native boolean[] consumeBooleans(int maxLength); + + @Override public native byte consumeByte(); + + @Override + public byte consumeByte(byte min, byte max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %d, max: %d)", min, max)); + } + return consumeByteUnchecked(min, max); + } + + @Override public native short consumeShort(); + + @Override + public short consumeShort(short min, short max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %d, max: %d)", min, max)); + } + return consumeShortUnchecked(min, max); + } + + @Override public native short[] consumeShorts(int maxLength); + + @Override public native int consumeInt(); + + @Override + public int consumeInt(int min, int max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %d, max: %d)", min, max)); + } + return consumeIntUnchecked(min, max); + } + + @Override public native int[] consumeInts(int maxLength); + + @Override public native long consumeLong(); + + @Override + public long consumeLong(long min, long max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %d, max: %d)", min, max)); + } + return consumeLongUnchecked(min, max); + } + + @Override public native long[] consumeLongs(int maxLength); + + @Override public native float consumeFloat(); + + @Override public native float consumeRegularFloat(); + + @Override + public float consumeRegularFloat(float min, float max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %f, max: %f)", min, max)); + } + return consumeRegularFloatUnchecked(min, max); + } + + @Override public native float consumeProbabilityFloat(); + + @Override public native double consumeDouble(); + + @Override + public double consumeRegularDouble(double min, double max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %f, max: %f)", min, max)); + } + return consumeRegularDoubleUnchecked(min, max); + } + + @Override public native double consumeRegularDouble(); + + @Override public native double consumeProbabilityDouble(); + + @Override public native char consumeChar(); + + @Override + public char consumeChar(char min, char max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %c, max: %c)", min, max)); + } + return consumeCharUnchecked(min, max); + } + + @Override public native char consumeCharNoSurrogates(); + + @Override public native String consumeAsciiString(int maxLength); + + @Override public native String consumeString(int maxLength); + + @Override public native String consumeRemainingAsAsciiString(); + + @Override public native String consumeRemainingAsString(); + + @Override public native byte[] consumeBytes(int maxLength); + + @Override public native byte[] consumeRemainingAsBytes(); + + @Override public native int remainingBytes(); + + private native byte consumeByteUnchecked(byte min, byte max); + private native short consumeShortUnchecked(short min, short max); + private native char consumeCharUnchecked(char min, char max); + private native int consumeIntUnchecked(int min, int max); + private native long consumeLongUnchecked(long min, long max); + private native float consumeRegularFloatUnchecked(float min, float max); + private native double consumeRegularDoubleUnchecked(double min, double max); +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/OfflineInstrumentor.java b/src/main/java/com/code_intelligence/jazzer/driver/OfflineInstrumentor.java new file mode 100644 index 00000000..3e779d42 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/OfflineInstrumentor.java @@ -0,0 +1,179 @@ +// Copyright 2021 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.driver; + +import com.code_intelligence.jazzer.agent.AgentInstaller; +import com.code_intelligence.jazzer.utils.Log; +import com.code_intelligence.jazzer.utils.ZipUtils; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.UnsupportedClassVersionError; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.zip.ZipOutputStream; + +public class OfflineInstrumentor { + /** + * Create a new jar file at <jazzer_path>/<jarBaseName>.instrumented.jar + * for each jar in passed in, with classes that have Jazzer instrumentation. + * + * @param jarLists list of jars to instrument + * @return a boolean representing the success status + */ + public static boolean instrumentJars(List<String> jarLists) { + AgentInstaller.install(Opt.hooks); + + // Clear Opt.dumpClassesDir before adding new instrumented classes + File dumpClassesDir = new File(Opt.dumpClassesDir); + if (dumpClassesDir.exists()) { + for (String fn : dumpClassesDir.list()) { + new File(Opt.dumpClassesDir, fn).delete(); + } + } + + List<String> errorMessages = new ArrayList<>(); + for (String jarPath : jarLists) { + String outputBaseName = jarPath; + if (outputBaseName.contains(File.separator)) { + outputBaseName = outputBaseName.substring( + outputBaseName.lastIndexOf(File.separator) + 1, outputBaseName.length()); + } + + if (outputBaseName.contains(".jar")) { + outputBaseName = outputBaseName.substring(0, outputBaseName.lastIndexOf(".jar")); + } + + Log.info("Instrumenting jar file: " + jarPath); + + try { + errorMessages = createInstrumentedClasses(jarPath); + } catch (IOException e) { + errorMessages.add("Failed to instrument jar: " + jarPath + + ". Please ensure the file at this location is a jar file. Error Message: " + e); + continue; + } + + try { + createInstrumentedJar(jarPath, Opt.dumpClassesDir + File.separator + outputBaseName, + outputBaseName + ".instrumented.jar"); + } catch (Exception e) { + errorMessages.add("Failed to instrument jar: " + jarPath + ". Error: " + e); + } + } + + // Log all errors at the end + for (String error : errorMessages) { + Log.error(error); + } + + return errorMessages.isEmpty(); + } + + /** + * Loops over all classes in jar file and adds instrumentation. The output + * of the instrumented classes will be at --dump-classes-dir + * + * @param jarPath a path to a jar file to instrument. + * @return a list of errors that were hit when trying to instrument all classes in jar + */ + private static List<String> createInstrumentedClasses(String jarPath) throws IOException { + List<String> errorMessages = new ArrayList<>(); + List<String> allClasses = new ArrayList<>(); + + // Collect all classes for jar file + try (JarFile jarFile = new JarFile(jarPath)) { + Enumeration<JarEntry> allEntries = jarFile.entries(); + while (allEntries.hasMoreElements()) { + JarEntry entry = allEntries.nextElement(); + if (entry.isDirectory()) { + continue; + } + + String name = entry.getName(); + if (!name.endsWith(".class")) { + Log.info("Skipping instrumenting file: " + name); + continue; + } + + String className = name.substring(0, name.lastIndexOf(".class")); + className = className.replace('/', '.'); + allClasses.add(className); + Log.info("Found class: " + className); + } + } + + // No classes found, so none to load. Return errors + if (allClasses.size() == 0) { + errorMessages.add("Classes is empty for jar: " + jarPath); + return errorMessages; + } + + // Create class loader to load in all classes + File file = new File(jarPath); + URL url = file.toURI().toURL(); + URL[] urls = new URL[] {url}; + ClassLoader cl = new URLClassLoader(urls); + + // Loop through all files and load in all classes, agent will instrument them as they load + for (String className : allClasses) { + try { + cl.loadClass(className); + } catch (UnsupportedClassVersionError ucve) { + // The classes will still get instrumented here, but warn so the user knows something + // happened + Log.warn(ucve.toString()); + } catch (Throwable e) { + // Catch all exceptions/errors and keep instrumenting to give user the option to manually + // fix one offs if possible + errorMessages.add("Failed to instrument class: " + className + ". Error: " + e); + } + } + + return errorMessages; + } + + /** + * This will create a new jar out of specified original jar and the merge in the instrumented + * classes from the specified instrumented classes dir + * + * @param originalJarPath a path to the original jar. + * @param instrumentedClassesDir a path to the instrumented classes dir. + * @param outputZip output file. + */ + private static void createInstrumentedJar( + String originalJarPath, String instrumentedClassesDir, String outputZip) throws IOException { + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outputZip))) { + Set<String> dirFilesToSkip = new HashSet<>(); + dirFilesToSkip.add(".original.class"); + dirFilesToSkip.add(".failed.class"); + Set<String> filesMerged = + ZipUtils.mergeDirectoryToZip(instrumentedClassesDir, zos, dirFilesToSkip); + + ZipUtils.mergeZipToZip(originalJarPath, zos, filesMerged); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/Opt.java b/src/main/java/com/code_intelligence/jazzer/driver/Opt.java new file mode 100644 index 00000000..f1a45c3b --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/Opt.java @@ -0,0 +1,220 @@ +/* + * Copyright 2022 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.driver; + +import static com.code_intelligence.jazzer.Constants.JAZZER_VERSION; +import static com.code_intelligence.jazzer.driver.OptParser.boolSetting; +import static com.code_intelligence.jazzer.driver.OptParser.ignoreSetting; +import static com.code_intelligence.jazzer.driver.OptParser.lazyStringListSetting; +import static com.code_intelligence.jazzer.driver.OptParser.stringListSetting; +import static com.code_intelligence.jazzer.driver.OptParser.stringSetting; +import static com.code_intelligence.jazzer.driver.OptParser.uint64Setting; +import static java.lang.System.exit; +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableSet; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; +import static java.util.stream.Stream.concat; + +import com.code_intelligence.jazzer.utils.Log; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * Static options that determine the runtime behavior of the fuzzer, set via Java properties. + * + * <p>Each option corresponds to a command-line argument of the driver of the same name. + * + * <p>Every public field should be deeply immutable. + */ +public final class Opt { + static { + if (Opt.class.getClassLoader() == null) { + throw new IllegalStateException("Opt should not be loaded in the bootstrap class loader"); + } + } + + static { + // We additionally list system properties supported by the Jazzer JUnit engine that do not + // directly map to arguments. These are not shown in help texts. + ignoreSetting("instrument"); + ignoreSetting("valueprofile"); + // The following arguments are interpreted by the native launcher only. They do appear in the + // help text, but aren't read by the driver. + stringListSetting("jvm_args", + "Arguments to pass to the JVM (separator can be escaped with '\\', native launcher only)"); + stringListSetting("additional_jvm_args", + "Additional arguments to pass to the JVM (separator can be escaped with '\\', native launcher only)"); + stringSetting( + "agent_path", null, "Custom path to jazzer_agent_deploy.jar (native launcher only)"); + // The following arguments are interpreted by the Jazzer main class directly as they require + // starting Jazzer as a subprocess. + boolSetting( + "asan", false, "Allow fuzzing of native libraries compiled with '-fsanitize=address'"); + boolSetting( + "ubsan", false, "Allow fuzzing of native libraries compiled with '-fsanitize=undefined'"); + boolSetting("native", false, + "Allow fuzzing of native libraries compiled with '-fsanitize=fuzzer' (implied by --asan and --ubsan)"); + // Options currently used by Android only + stringSetting("android_init_options", null, + "Which libraries to use when initializing ART (native launcher only)"); + boolSetting("hwasan", false, "Allow fuzzing of native libraries compiled with hwasan"); + } + + public static final String autofuzz = stringSetting("autofuzz", "", + "Fully qualified reference (optionally with a Javadoc-style signature) to a " + + "method on the class path to be fuzzed with automatically generated arguments " + + "(examples: java.lang.System.out::println, java.lang.String::new(byte[]))"); + public static final List<String> autofuzzIgnore = stringListSetting("autofuzz_ignore", ',', + "Fully qualified names of exception classes to ignore during fuzzing"); + public static final String coverageDump = stringSetting("coverage_dump", "", + "Path to write a JaCoCo .exec file to when the fuzzer exits (if non-empty)"); + public static final String coverageReport = stringSetting("coverage_report", "", + "Path to write a human-readable coverage report to when the fuzzer exits (if non-empty)"); + public static final List<String> customHooks = + stringListSetting("custom_hooks", "Names of classes to load custom hooks from"); + public static final List<String> disabledHooks = stringListSetting("disabled_hooks", + "Names of classes from which hooks (custom or built-in) should not be loaded from"); + public static final String dumpClassesDir = stringSetting( + "dump_classes_dir", "", "Directory to dump instrumented .class files into (if non-empty)"); + public static final boolean experimentalMutator = + boolSetting("experimental_mutator", false, "Use an experimental structured mutator"); + public static final long experimentalCrossOverFrequency = uint64Setting( + "experimental_cross_over_frequency", 100, + "(Used in experimental mutator) Frequency of cross-over mutations actually being executed " + + "when the cross-over function is picked by the underlying fuzzing engine (~1/2 of all mutations), " + + "other invocations perform type specific mutations via the experimental mutator. " + + "(0 = disabled, 1 = every call, 2 = every other call, etc.)."); + public static final boolean hooks = boolSetting( + "hooks", true, "Apply fuzzing instrumentation (use 'trace' for finer-grained control)"); + public static final String idSyncFile = stringSetting("id_sync_file", null, null); + public static final Set<Long> ignore = + unmodifiableSet(stringListSetting("ignore", ',', + "Hex strings representing deduplication tokens of findings that should be ignored") + .stream() + .map(token -> Long.parseUnsignedLong(token, 16)) + .collect(toSet())); + public static final long keepGoing = uint64Setting( + "keep_going", 1, "Number of distinct findings after which the fuzzer should stop"); + public static final String reproducerPath = stringSetting("reproducer_path", ".", + "Directory in which stand-alone Java reproducers are stored for each finding"); + public static final String targetClass = stringSetting("target_class", "", + "Fully qualified name of the fuzz target class (required unless --autofuzz is specified)"); + // Used to disambiguate between multiple methods annotated with @FuzzTest in the target class. + public static final String targetMethod = stringSetting("target_method", "", null); + public static final List<String> trace = stringListSetting("trace", + "Types of instrumentation to apply: cmp, cov, div, gep (disabled by default), indir, native"); + + // When Jazzer is executed from the command line, these settings are potentially modified by + // JUnit's AgentConfigurator after the Driver has initialized Opt, which would result in stale + // values being read if the settings weren't evaluated lazily. + // TODO: Look into making all settings lazy, but verify that their value never changes after they + // have been read once. + public static final Supplier<List<String>> customHookIncludes = + lazyStringListSetting("custom_hook_includes", + "Glob patterns matching names of classes to instrument with hooks (custom and built-in)"); + public static final Supplier<List<String>> customHookExcludes = lazyStringListSetting( + "custom_hook_excludes", + "Glob patterns matching names of classes that should not be instrumented with hooks (custom and built-in)"); + public static final Supplier<List<String>> instrumentationIncludes = + lazyStringListSetting("instrumentation_includes", + "Glob patterns matching names of classes to instrument for fuzzing"); + public static final Supplier<List<String>> instrumentationExcludes = + lazyStringListSetting("instrumentation_excludes", + "Glob patterns matching names of classes that should not be instrumented for fuzzing"); + // The values of this setting depends on autofuzz. + public static final List<String> targetArgs = autofuzz.isEmpty() + ? stringListSetting( + "target_args", ' ', "Arguments to pass to the fuzz target's fuzzerInitialize method") + : unmodifiableList(concat(Stream.of(autofuzz), autofuzzIgnore.stream()).collect(toList())); + + // Default to false if hooks is false to mimic the original behavior of the native fuzz target + // runner, but still support hooks = false && dedup = true. + public static final boolean dedup = + boolSetting("dedup", hooks, "Compute and print a deduplication token for every finding"); + + public static final String androidBootclassJarPath = stringSetting("android_bootclass_jar_path", + null, + "Full path to booclass jar path that will be used on Android runs. If you are using the launcher this will be set for you."); + + public static final String androidBootclassClassesOverrides = stringSetting( + "android_bootpath_classes_overrides", null, + "Used for fuzzing classes loaded in through the bootstrap class loader on Android. Full path to jar file with the instrumented versions of the classes you want to override."); + + // Whether hook instrumentation should add a check for JazzerInternal#hooksEnabled before + // executing hooks. Used to disable hooks during non-fuzz JUnit tests. + public static final boolean conditionalHooks = + boolSetting("internal.conditional_hooks", false, null); + + static final boolean mergeInner = boolSetting("internal.merge_inner", false, null); + + private static final boolean help = + boolSetting("help", false, "Show this list of all available arguments"); + private static final boolean version = boolSetting("version", false, "Print version information"); + + // Methods below currently used by Android only + public static final List<String> cp = + stringListSetting("cp", "The class path to use for fuzzing (native launcher only)"); + + public static final List<String> additionalClassesExcludes = + stringListSetting("additional_classes_excludes", + "Glob patterns matching names of classes from Java that are not in your jar file, " + + "but may be included in your program"); + + // Default to false. Sets if fuzzing is taking place on Android device (virtual or physical) + public static final boolean isAndroid = + boolSetting("android", false, "Jazzer is running on Android"); + + // Some scenarios require instrumenting the jar before fuzzing begins + public static final List<String> instrumentOnly = stringListSetting("instrument_only", ',', + "Comma separated list of jar files to instrument. No fuzzing is performed."); + + static { + OptParser.failOnUnknownArgument(); + + if (help) { + Log.println(OptParser.getHelpText()); + exit(0); + } + if (version) { + Log.println("Jazzer v" + JAZZER_VERSION); + exit(0); + } + if (!targetClass.isEmpty() && !autofuzz.isEmpty()) { + Log.error("--target_class and --autofuzz cannot be specified together"); + exit(1); + } + if (!stringListSetting("target_args", ' ', null).isEmpty() && !autofuzz.isEmpty()) { + Log.error("--target_args and --autofuzz cannot be specified together"); + exit(1); + } + if (autofuzz.isEmpty() && !autofuzzIgnore.isEmpty()) { + Log.error("--autofuzz_ignore requires --autofuzz"); + exit(1); + } + if ((!ignore.isEmpty() || keepGoing > 1) && !dedup) { + Log.error("--nodedup is not supported with --ignore or --keep_going"); + exit(1); + } + if (!instrumentOnly.isEmpty() && dumpClassesDir.isEmpty()) { + Log.error("--dump_classes_dir must be set with --instrument_only"); + exit(1); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/OptParser.java b/src/main/java/com/code_intelligence/jazzer/driver/OptParser.java new file mode 100644 index 00000000..ab096f2b --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/OptParser.java @@ -0,0 +1,212 @@ +/* + * Copyright 2022 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.driver; + +import static java.lang.System.exit; + +import com.code_intelligence.jazzer.utils.Log; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +final class OptParser { + private static final String[] HELP_HEADER = new String[] { + "A coverage-guided, in-process fuzzer for the JVM", + "", + "Usage:", + String.format( + " java -cp jazzer.jar[%cclasspath_entries] com.code_intelligence.jazzer.Jazzer --target_class=<target class> [args...]", + File.separatorChar), + String.format( + " java -cp jazzer.jar[%cclasspath_entries] com.code_intelligence.jazzer.Jazzer --autofuzz=<method reference> [args...]", + File.separatorChar), + "", + "In addition to the options listed below, Jazzer also accepts all", + "libFuzzer options described at:", + " https://llvm.org/docs/LibFuzzer.html#options", + "", + "Options:", + }; + private static final String OPTIONS_PREFIX = "jazzer."; + + // All supported arguments are added to this set by the individual *Setting methods. + private static final Map<String, OptDetails> knownArgs = new TreeMap<>(); + + static String getHelpText() { + return Stream + .concat(Arrays.stream(HELP_HEADER), + knownArgs.values().stream().filter(Objects::nonNull).map(OptDetails::toString)) + .collect(Collectors.joining("\n\n")); + } + + static void ignoreSetting(String name) { + knownArgs.put(name, null); + } + + static String stringSetting(String name, String defaultValue, String description) { + knownArgs.put(name, OptDetails.create(name, "string", defaultValue, description)); + return System.getProperty(OPTIONS_PREFIX + name, defaultValue); + } + + static List<String> stringListSetting(String name, String description) { + return lazyStringListSetting(name, description).get(); + } + + static List<String> stringListSetting(String name, char separator, String description) { + return lazyStringListSetting(name, separator, description).get(); + } + + static Supplier<List<String>> lazyStringListSetting(String name, String description) { + return lazyStringListSetting(name, File.pathSeparatorChar, description); + } + + static Supplier<List<String>> lazyStringListSetting( + String name, char separator, String description) { + knownArgs.put(name, + OptDetails.create( + name, String.format("list separated by '%c'", separator), "", description)); + return () -> { + String value = System.getProperty(OPTIONS_PREFIX + name); + if (value == null || value.isEmpty()) { + return Collections.emptyList(); + } + return splitOnUnescapedSeparator(value, separator); + }; + } + + static boolean boolSetting(String name, boolean defaultValue, String description) { + knownArgs.put( + name, OptDetails.create(name, "boolean", Boolean.toString(defaultValue), description)); + String value = System.getProperty(OPTIONS_PREFIX + name); + if (value == null) { + return defaultValue; + } + return Boolean.parseBoolean(value); + } + + static long uint64Setting(String name, long defaultValue, String description) { + knownArgs.put( + name, OptDetails.create(name, "uint64", Long.toUnsignedString(defaultValue), description)); + String value = System.getProperty(OPTIONS_PREFIX + name); + if (value == null) { + return defaultValue; + } + return Long.parseUnsignedLong(value, 10); + } + + static void failOnUnknownArgument() { + System.getProperties() + .keySet() + .stream() + .map(key -> (String) key) + .filter(key -> key.startsWith("jazzer.")) + .map(key -> key.substring("jazzer.".length())) + .filter(key -> !key.startsWith("internal.")) + .filter(key -> !knownArgs.containsKey(key)) + .findFirst() + .ifPresent(unknownArg -> { + Log.error(String.format( + "Unknown argument '--%1$s' or property 'jazzer.%1$s' (list all available arguments with --help)", + unknownArg)); + exit(1); + }); + } + + /** + * Split value into non-empty takens separated by separator. Backslashes can be used to escape + * separators (or backslashes). + * + * @param value the string to split + * @param separator a single character to split on (backslash is not allowed) + * @return an immutable list of tokens obtained by splitting value on separator + */ + static List<String> splitOnUnescapedSeparator(String value, char separator) { + if (separator == '\\') { + throw new IllegalArgumentException("separator '\\' is not supported"); + } + ArrayList<String> tokens = new ArrayList<>(); + StringBuilder currentToken = new StringBuilder(); + boolean inEscapeState = false; + for (int pos = 0; pos < value.length(); pos++) { + char c = value.charAt(pos); + if (inEscapeState) { + currentToken.append(c); + inEscapeState = false; + } else if (c == '\\') { + inEscapeState = true; + } else if (c == separator) { + // Do not emit empty tokens between consecutive separators. + if (currentToken.length() > 0) { + tokens.add(currentToken.toString()); + } + currentToken.setLength(0); + } else { + currentToken.append(c); + } + } + if (currentToken.length() > 0) { + tokens.add(currentToken.toString()); + } + return Collections.unmodifiableList(tokens); + } + + private static final class OptDetails { + final String name; + final String type; + final String defaultValue; + final String description; + + private OptDetails(String name, String type, String defaultValue, String description) { + this.name = name; + this.type = type; + this.defaultValue = defaultValue; + this.description = description; + } + + static OptDetails create(String name, String type, String defaultValue, String description) { + if (description == null) { + return null; + } + return new OptDetails(checkNotNullOrEmpty(name, "name"), checkNotNullOrEmpty(type, "type"), + defaultValue, checkNotNullOrEmpty(description, "description")); + } + + @Override + public String toString() { + return String.format( + "--%s (%s, default: \"%s\")%n %s", name, type, defaultValue, description); + } + + private static String checkNotNullOrEmpty(String arg, String name) { + if (arg == null) { + throw new NullPointerException(name + " must not be null"); + } + if (arg.isEmpty()) { + throw new NullPointerException(name + " must not be empty"); + } + return arg; + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/RecordingFuzzedDataProvider.java b/src/main/java/com/code_intelligence/jazzer/driver/RecordingFuzzedDataProvider.java new file mode 100644 index 00000000..6593f0d9 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/RecordingFuzzedDataProvider.java @@ -0,0 +1,213 @@ +// Copyright 2021 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.driver; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.Base64; + +// Wraps the native FuzzedDataProviderImpl and serializes all its return values +// into a Base64-encoded string. +public final class RecordingFuzzedDataProvider implements FuzzedDataProvider { + private final FuzzedDataProvider target; + private final ArrayList<Object> recordedReplies = new ArrayList<>(); + + private RecordingFuzzedDataProvider(FuzzedDataProvider target) { + this.target = target; + } + + public static FuzzedDataProvider makeFuzzedDataProviderProxy(FuzzedDataProvider target) { + return new RecordingFuzzedDataProvider(target); + } + + public static String serializeFuzzedDataProviderProxy(FuzzedDataProvider proxy) + throws IOException { + return ((RecordingFuzzedDataProvider) proxy).serialize(); + } + + private <T> T recordAndReturn(T object) { + recordedReplies.add(object); + return object; + } + + private String serialize() throws IOException { + byte[] rawOut; + try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream()) { + try (ObjectOutputStream objectStream = new ObjectOutputStream(byteStream)) { + objectStream.writeObject(recordedReplies); + } + rawOut = byteStream.toByteArray(); + } + return Base64.getEncoder().encodeToString(rawOut); + } + + @Override + public boolean consumeBoolean() { + return recordAndReturn(target.consumeBoolean()); + } + + @Override + public boolean[] consumeBooleans(int maxLength) { + return recordAndReturn(target.consumeBooleans(maxLength)); + } + + @Override + public byte consumeByte() { + return recordAndReturn(target.consumeByte()); + } + + @Override + public byte consumeByte(byte min, byte max) { + return recordAndReturn(target.consumeByte(min, max)); + } + + @Override + public byte[] consumeBytes(int maxLength) { + return recordAndReturn(target.consumeBytes(maxLength)); + } + + @Override + public byte[] consumeRemainingAsBytes() { + return recordAndReturn(target.consumeRemainingAsBytes()); + } + + @Override + public short consumeShort() { + return recordAndReturn(target.consumeShort()); + } + + @Override + public short consumeShort(short min, short max) { + return recordAndReturn(target.consumeShort(min, max)); + } + + @Override + public short[] consumeShorts(int maxLength) { + return recordAndReturn(target.consumeShorts(maxLength)); + } + + @Override + public int consumeInt() { + return recordAndReturn(target.consumeInt()); + } + + @Override + public int consumeInt(int min, int max) { + return recordAndReturn(target.consumeInt(min, max)); + } + + @Override + public int[] consumeInts(int maxLength) { + return recordAndReturn(target.consumeInts(maxLength)); + } + + @Override + public long consumeLong() { + return recordAndReturn(target.consumeLong()); + } + + @Override + public long consumeLong(long min, long max) { + return recordAndReturn(target.consumeLong(min, max)); + } + + @Override + public long[] consumeLongs(int maxLength) { + return recordAndReturn(target.consumeLongs(maxLength)); + } + + @Override + public float consumeFloat() { + return recordAndReturn(target.consumeFloat()); + } + + @Override + public float consumeRegularFloat() { + return recordAndReturn(target.consumeRegularFloat()); + } + + @Override + public float consumeRegularFloat(float min, float max) { + return recordAndReturn(target.consumeRegularFloat(min, max)); + } + + @Override + public float consumeProbabilityFloat() { + return recordAndReturn(target.consumeProbabilityFloat()); + } + + @Override + public double consumeDouble() { + return recordAndReturn(target.consumeDouble()); + } + + @Override + public double consumeRegularDouble() { + return recordAndReturn(target.consumeRegularDouble()); + } + + @Override + public double consumeRegularDouble(double min, double max) { + return recordAndReturn(target.consumeRegularDouble(min, max)); + } + + @Override + public double consumeProbabilityDouble() { + return recordAndReturn(target.consumeProbabilityDouble()); + } + + @Override + public char consumeChar() { + return recordAndReturn(target.consumeChar()); + } + + @Override + public char consumeChar(char min, char max) { + return recordAndReturn(target.consumeChar(min, max)); + } + + @Override + public char consumeCharNoSurrogates() { + return recordAndReturn(target.consumeCharNoSurrogates()); + } + + @Override + public String consumeString(int maxLength) { + return recordAndReturn(target.consumeString(maxLength)); + } + + @Override + public String consumeRemainingAsString() { + return recordAndReturn(target.consumeRemainingAsString()); + } + + @Override + public String consumeAsciiString(int maxLength) { + return recordAndReturn(target.consumeAsciiString(maxLength)); + } + + @Override + public String consumeRemainingAsAsciiString() { + return recordAndReturn(target.consumeRemainingAsAsciiString()); + } + + @Override + public int remainingBytes() { + return recordAndReturn(target.remainingBytes()); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/Reproducer.java.tmpl b/src/main/java/com/code_intelligence/jazzer/driver/Reproducer.java.tmpl new file mode 100644 index 00000000..3c441756 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/Reproducer.java.tmpl @@ -0,0 +1,28 @@ +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class Crash_%1$s { + static final String base64Bytes = String.join("", "%2$s"); + + public static void main(String[] args) throws Throwable { + Crash_%1$s.class.getClassLoader().setDefaultAssertionStatus(true); + try { + Method fuzzerInitialize = %3$s.class.getMethod("fuzzerInitialize"); + fuzzerInitialize.invoke(null); + } catch (NoSuchMethodException ignored) { + try { + Method fuzzerInitialize = %3$s.class.getMethod("fuzzerInitialize", String[].class); + fuzzerInitialize.invoke(null, (Object) args); + } catch (NoSuchMethodException ignored1) { + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + System.exit(1); + } + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + System.exit(1); + } + %4$s + %3$s.fuzzerTestOneInput(input); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/ReproducerTemplate.java b/src/main/java/com/code_intelligence/jazzer/driver/ReproducerTemplate.java new file mode 100644 index 00000000..a69a8dba --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/ReproducerTemplate.java @@ -0,0 +1,85 @@ +/* + * Copyright 2022 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.driver; + +import com.code_intelligence.jazzer.utils.Log; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.stream.Collectors; + +final class ReproducerTemplate { + // A constant pool CONSTANT_Utf8_info entry should be able to hold data of size + // uint16, but somehow this does not seem to be the case and leads to invalid + // code crash reproducer code. Reducing the size by one resolves the problem. + private static final int DATA_CHUNK_MAX_LENGTH = Short.MAX_VALUE - 1; + private static final String RAW_BYTES_INPUT = + "byte[] input = java.util.Base64.getDecoder().decode(base64Bytes);"; + private static final String FUZZED_DATA_PROVIDER_INPUT = + "com.code_intelligence.jazzer.api.CannedFuzzedDataProvider input = new com.code_intelligence.jazzer.api.CannedFuzzedDataProvider(base64Bytes);"; + + private final String targetClass; + private final boolean useFuzzedDataProvider; + + public ReproducerTemplate(String targetClass, boolean useFuzzedDataProvider) { + this.targetClass = targetClass; + this.useFuzzedDataProvider = useFuzzedDataProvider; + } + + /** + * Emits a Java reproducer to {@code Crash_HASH.java} in {@code Opt.reproducerPath}. + * + * @param data the Base64-encoded data to emit as a string literal + * @param sha the SHA1 hash of the raw fuzzer input + */ + public void dumpReproducer(String data, String sha) { + String targetArg = useFuzzedDataProvider ? FUZZED_DATA_PROVIDER_INPUT : RAW_BYTES_INPUT; + String template = new BufferedReader( + new InputStreamReader(ReproducerTemplate.class.getResourceAsStream("Reproducer.java.tmpl"), + StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + String chunkedData = chunkStringLiteral(data); + String javaSource = String.format(template, sha, chunkedData, targetClass, targetArg); + Path javaPath = Paths.get(Opt.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.printStackTrace(); + } + Log.println(String.format( + "reproducer_path='%s'; Java reproducer written to %s%n", Opt.reproducerPath, javaPath)); + } + + // The serialization of recorded FuzzedDataProvider invocations can get too long to be emitted + // into the template as a single String literal. This is mitigated by chunking the data and + // concatenating it again in the generated code. + private String chunkStringLiteral(String data) { + ArrayList<String> chunks = new ArrayList<>(); + for (int i = 0; i <= data.length() / DATA_CHUNK_MAX_LENGTH; i++) { + chunks.add(data.substring( + i * DATA_CHUNK_MAX_LENGTH, Math.min((i + 1) * DATA_CHUNK_MAX_LENGTH, data.length()))); + } + return String.join("\", \"", chunks); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/SignalHandler.java b/src/main/java/com/code_intelligence/jazzer/driver/SignalHandler.java new file mode 100644 index 00000000..215a0479 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/SignalHandler.java @@ -0,0 +1,31 @@ +// Copyright 2021 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.driver; + +import com.github.fmeum.rules_jni.RulesJni; +import sun.misc.Signal; + +public final class SignalHandler { + static { + RulesJni.loadLibrary("jazzer_signal_handler", SignalHandler.class); + Signal.handle(new Signal("INT"), sig -> handleInterrupt()); + } + + public static void initialize() { + // Implicitly runs the static initializer. + } + + private static native void handleInterrupt(); +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/junit/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/driver/junit/BUILD.bazel new file mode 100644 index 00000000..c715365b --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/junit/BUILD.bazel @@ -0,0 +1,30 @@ +java_library( + name = "junit_runner", + srcs = ["JUnitRunner.java"], + visibility = ["//src/main/java/com/code_intelligence/jazzer/driver:__pkg__"], + deps = [ + ":exit_code_exception", + ":junit_compile_only", + "//src/main/java/com/code_intelligence/jazzer/driver:constants", + "//src/main/java/com/code_intelligence/jazzer/driver:exception_utils", + "//src/main/java/com/code_intelligence/jazzer/driver:fuzz_target_runner", + "//src/main/java/com/code_intelligence/jazzer/driver:opt", + "//src/main/java/com/code_intelligence/jazzer/utils:log", + "@maven//:org_junit_platform_junit_platform_engine", + ], +) + +java_library( + name = "exit_code_exception", + srcs = ["ExitCodeException.java"], + visibility = ["//src/main/java/com/code_intelligence/jazzer/junit:__pkg__"], +) + +java_library( + name = "junit_compile_only", + neverlink = True, + exports = [ + "@maven//:org_junit_jupiter_junit_jupiter_engine", + "@maven//:org_junit_platform_junit_platform_launcher", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/driver/junit/ExitCodeException.java b/src/main/java/com/code_intelligence/jazzer/driver/junit/ExitCodeException.java new file mode 100644 index 00000000..662fb9b1 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/junit/ExitCodeException.java @@ -0,0 +1,26 @@ +/* + * 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.driver.junit; + +public final class ExitCodeException extends Exception { + public final int exitCode; + + public ExitCodeException(String message, int exitCode) { + super(message); + this.exitCode = exitCode; + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/driver/junit/JUnitRunner.java b/src/main/java/com/code_intelligence/jazzer/driver/junit/JUnitRunner.java new file mode 100644 index 00000000..5bba4342 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/driver/junit/JUnitRunner.java @@ -0,0 +1,165 @@ +/* + * 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.driver.junit; + +import static com.code_intelligence.jazzer.driver.Constants.JAZZER_FINDING_EXIT_CODE; +import static com.code_intelligence.jazzer.driver.FuzzTargetRunner.printCrashingInput; +import static org.junit.platform.engine.FilterResult.includedIf; +import static org.junit.platform.engine.TestExecutionResult.Status.ABORTED; +import static org.junit.platform.engine.TestExecutionResult.Status.FAILED; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.launcher.TagFilter.includeTags; + +import com.code_intelligence.jazzer.driver.ExceptionUtils; +import com.code_intelligence.jazzer.driver.Opt; +import com.code_intelligence.jazzer.utils.Log; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.jupiter.engine.JupiterTestEngine; +import org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.PostDiscoveryFilter; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.core.LauncherConfig; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; + +public final class JUnitRunner { + private final Launcher launcher; + private final TestPlan testPlan; + + private JUnitRunner(Launcher launcher, TestPlan testPlan) { + this.launcher = launcher; + this.testPlan = testPlan; + } + + // Detects the presence of both the JUnit launcher and the Jupiter engine on the classpath. + public static boolean isSupported() { + try { + Class.forName("org.junit.platform.launcher.LauncherDiscoveryRequest"); + Class.forName("org.junit.jupiter.engine.JupiterTestEngine"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + public static Optional<JUnitRunner> create(String testClassName, List<String> libFuzzerArgs) { + // We want the test execution to be as lightweight as possible, so disable all auto-discover and + // only register the test engine we are using for @FuzzTest, JUnit Jupiter. + LauncherConfig config = LauncherConfig.builder() + .addTestEngines(new JupiterTestEngine()) + .enableLauncherDiscoveryListenerAutoRegistration(false) + .enableLauncherSessionListenerAutoRegistration(false) + .enablePostDiscoveryFilterAutoRegistration(false) + .enableTestEngineAutoRegistration(false) + .enableTestExecutionListenerAutoRegistration(false) + .build(); + + Map<String, String> indexedArgs = + IntStream.range(0, libFuzzerArgs.size()) + .boxed() + .collect(Collectors.toMap(i -> "jazzer.internal.arg." + i, libFuzzerArgs::get)); + + LauncherDiscoveryRequestBuilder requestBuilder = + LauncherDiscoveryRequestBuilder.request() + .configurationParameter("jazzer.internal.commandLine", "true") + .configurationParameters(indexedArgs) + .selectors(selectClass(testClassName)) + .filters(includeTags("jazzer")); + if (!Opt.targetMethod.isEmpty()) { + // HACK: This depends on JUnit internals as we need to filter by method name without having to + // specify the parameter types of the method. + requestBuilder.filters((PostDiscoveryFilter) testDescriptor + -> includedIf(!(testDescriptor instanceof MethodBasedTestDescriptor) + || ((MethodBasedTestDescriptor) testDescriptor) + .getTestMethod() + .getName() + .equals(Opt.targetMethod))); + } + LauncherDiscoveryRequest request = requestBuilder.build(); + Launcher launcher = LauncherFactory.create(config); + TestPlan testPlan = launcher.discover(request); + if (!testPlan.containsTests()) { + return Optional.empty(); + } + return Optional.of(new JUnitRunner(launcher, testPlan)); + } + + public int run() { + AtomicReference<TestExecutionResult> resultHolder = + new AtomicReference<>(TestExecutionResult.successful()); + launcher.execute(testPlan, new TestExecutionListener() { + @Override + public void executionFinished( + TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + // Lifecycle methods can fail too, which results in failed execution results on container + // nodes. We keep the last failing one with a stack trace. For tests, we also keep the stack + // traces of aborted tests so that we can show a warning. In JUnit Jupiter, tests and + // containers always fail with a throwable: + // https://github.com/junit-team/junit5/blob/ac31e9a7d58973db73496244dab4defe17ae563e/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ThrowableCollector.java#LL176C37-L176C37 + if ((testIdentifier.isTest() && testExecutionResult.getThrowable().isPresent()) + || testExecutionResult.getStatus() == FAILED) { + resultHolder.set(testExecutionResult); + } + if (testExecutionResult.getStatus() == FAILED + && testExecutionResult.getThrowable().isPresent()) { + resultHolder.set(testExecutionResult); + } + } + + @Override + public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry entry) { + entry.getKeyValuePairs().values().forEach(Log::info); + } + }); + + TestExecutionResult result = resultHolder.get(); + if (result.getStatus() != FAILED) { + // We do not generate a finding for Aborted tests (i.e. tests whose preconditions were not + // met) as such tests also wouldn't make a test run fail. + if (result.getStatus() == ABORTED) { + Log.warn("Fuzz test aborted", result.getThrowable().orElse(null)); + } + return 0; + } + + // Safe to unwrap as result is either TestExecutionResult.successful() (initial value) or has + // a throwable (set in the TestExecutionListener above). + Throwable throwable = result.getThrowable().get(); + if (throwable instanceof ExitCodeException) { + // Jazzer found a regular finding and printed it, so just return the exit code. + return ((ExitCodeException) throwable).exitCode; + } else { + // Jazzer didn't report a finding, but an afterAll-type function threw an exception. Report it + // as a finding, cleaning up the stack trace. + Log.finding(ExceptionUtils.preprocessThrowable(throwable)); + Log.println("== libFuzzer crashing input =="); + printCrashingInput(); + return JAZZER_FINDING_EXIT_CODE; + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel new file mode 100644 index 00000000..bbb449a4 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -0,0 +1,41 @@ +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") +load("//bazel:kotlin.bzl", "ktlint") + +kt_jvm_library( + name = "instrumentor", + srcs = [ + "ClassInstrumentor.kt", + "CoverageRecorder.kt", + "DescriptorUtils.kt", + "DeterministicRandom.kt", + "EdgeCoverageInstrumentor.kt", + "Hook.kt", + "HookInstrumentor.kt", + "HookMethodVisitor.kt", + "Hooks.kt", + "Instrumentor.kt", + "StaticMethodStrategy.java", + "TraceDataFlowInstrumentor.kt", + ], + visibility = [ + "//src/jmh/java/com/code_intelligence/jazzer/instrumentor:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/driver:__pkg__", + "//src/test/java/com/code_intelligence/jazzer/instrumentor:__pkg__", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/api:hooks", + "//src/main/java/com/code_intelligence/jazzer/runtime:jazzer_bootstrap_compile_only", + "//src/main/java/com/code_intelligence/jazzer/utils", + "//src/main/java/com/code_intelligence/jazzer/utils:class_name_globber", + "//src/main/java/com/code_intelligence/jazzer/utils:log", + "@com_github_classgraph_classgraph//:classgraph", + "@com_github_jetbrains_kotlin//:kotlin-reflect", + "@jazzer_jacoco//:jacoco_internal", + "@org_ow2_asm_asm//jar", + "@org_ow2_asm_asm_commons//jar", + "@org_ow2_asm_asm_tree//jar", + ], +) + +ktlint() diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt new file mode 100644 index 00000000..a93e29c7 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt @@ -0,0 +1,55 @@ +// Copyright 2021 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.instrumentor + +import com.code_intelligence.jazzer.runtime.CoverageMap + +fun extractClassFileMajorVersion(classfileBuffer: ByteArray): Int { + return ((classfileBuffer[6].toInt() and 0xff) shl 8) or (classfileBuffer[7].toInt() and 0xff) +} + +class ClassInstrumentor(private val internalClassName: String, bytecode: ByteArray) { + + var instrumentedBytecode = bytecode + private set + + fun coverage(initialEdgeId: Int): Int { + val edgeCoverageInstrumentor = EdgeCoverageInstrumentor( + defaultEdgeCoverageStrategy, + defaultCoverageMap, + initialEdgeId, + ) + instrumentedBytecode = edgeCoverageInstrumentor.instrument(internalClassName, instrumentedBytecode) + return edgeCoverageInstrumentor.numEdges + } + + fun traceDataFlow(instrumentations: Set<InstrumentationType>) { + instrumentedBytecode = + TraceDataFlowInstrumentor(instrumentations).instrument(internalClassName, instrumentedBytecode) + } + + fun hooks(hooks: Iterable<Hook>, classWithHooksEnabledField: String?) { + instrumentedBytecode = HookInstrumentor( + hooks, + java6Mode = extractClassFileMajorVersion(instrumentedBytecode) < 51, + classWithHooksEnabledField = classWithHooksEnabledField, + ).instrument(internalClassName, instrumentedBytecode) + } + + companion object { + val defaultEdgeCoverageStrategy = StaticMethodStrategy() + val defaultCoverageMap = CoverageMap::class.java + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt new file mode 100644 index 00000000..56fb5725 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt @@ -0,0 +1,252 @@ +// Copyright 2021 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.instrumentor + +import com.code_intelligence.jazzer.runtime.CoverageMap +import com.code_intelligence.jazzer.third_party.org.jacoco.core.analysis.CoverageBuilder +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionData +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataStore +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataWriter +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.SessionInfo +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.data.CRC64 +import com.code_intelligence.jazzer.utils.ClassNameGlobber +import io.github.classgraph.ClassGraph +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream +import java.time.Instant +import java.util.UUID + +private data class InstrumentedClassInfo( + val classId: Long, + val initialEdgeId: Int, + val nextEdgeId: Int, + val bytecode: ByteArray, +) + +object CoverageRecorder { + var classNameGlobber = ClassNameGlobber(emptyList(), emptyList()) + private val instrumentedClassInfo = mutableMapOf<String, InstrumentedClassInfo>() + private var startTimestamp: Instant? = null + private val additionalCoverage = mutableSetOf<Int>() + + fun recordInstrumentedClass(internalClassName: String, bytecode: ByteArray, firstId: Int, numIds: Int) { + if (startTimestamp == null) { + startTimestamp = Instant.now() + } + instrumentedClassInfo[internalClassName] = InstrumentedClassInfo( + CRC64.classId(bytecode), + firstId, + firstId + numIds, + bytecode, + ) + } + + /** + * Manually records coverage IDs based on the current state of [CoverageMap]. + * Should be called after static initializers have run. + */ + @JvmStatic + fun updateCoveredIdsWithCoverageMap() { + additionalCoverage.addAll(CoverageMap.getCoveredIds()) + } + + /** + * [dumpCoverageReport] dumps a human-readable coverage report of files using any [coveredIds] to [dumpFileName]. + */ + @JvmStatic + @JvmOverloads + fun dumpCoverageReport(dumpFileName: String, coveredIds: IntArray = CoverageMap.getEverCoveredIds()) { + File(dumpFileName).bufferedWriter().use { writer -> + writer.write(computeFileCoverage(coveredIds)) + } + } + + private fun computeFileCoverage(coveredIds: IntArray): String { + fun Double.format(digits: Int) = "%.${digits}f".format(this) + val coverage = analyzeCoverage(coveredIds.toSet()) ?: return "No classes were instrumented" + return coverage.sourceFiles.joinToString( + "\n", + prefix = "Branch coverage:\n", + postfix = "\n\n", + ) { fileCoverage -> + val counter = fileCoverage.branchCounter + val percentage = 100 * counter.coveredRatio + "${fileCoverage.name}: ${counter.coveredCount}/${counter.totalCount} (${percentage.format(2)}%)" + } + coverage.sourceFiles.joinToString( + "\n", + prefix = "Line coverage:\n", + postfix = "\n\n", + ) { fileCoverage -> + val counter = fileCoverage.lineCounter + val percentage = 100 * counter.coveredRatio + "${fileCoverage.name}: ${counter.coveredCount}/${counter.totalCount} (${percentage.format(2)}%)" + } + coverage.sourceFiles.joinToString( + "\n", + prefix = "Incompletely covered lines:\n", + postfix = "\n\n", + ) { fileCoverage -> + "${fileCoverage.name}: " + (fileCoverage.firstLine..fileCoverage.lastLine).filter { + val instructions = fileCoverage.getLine(it).instructionCounter + instructions.coveredCount in 1 until instructions.totalCount + }.toString() + } + coverage.sourceFiles.joinToString( + "\n", + prefix = "Missed lines:\n", + ) { fileCoverage -> + "${fileCoverage.name}: " + (fileCoverage.firstLine..fileCoverage.lastLine).filter { + val instructions = fileCoverage.getLine(it).instructionCounter + instructions.coveredCount == 0 && instructions.totalCount > 0 + }.toString() + } + } + + /** + * [dumpJacocoCoverage] dumps the JaCoCo coverage of files using any [coveredIds] to [dumpFileName]. + * JaCoCo only exports coverage for files containing at least one coverage data point. The dump + * can be used by the JaCoCo report command to create reports also including not covered files. + */ + @JvmStatic + @JvmOverloads + fun dumpJacocoCoverage(dumpFileName: String, coveredIds: IntArray = CoverageMap.getEverCoveredIds()) { + FileOutputStream(dumpFileName).use { outStream -> + dumpJacocoCoverage(outStream, coveredIds) + } + } + + /** + * [dumpJacocoCoverage] dumps the JaCoCo coverage of files using any [coveredIds] to [outStream]. + */ + @JvmStatic + fun dumpJacocoCoverage(outStream: OutputStream, coveredIds: IntArray) { + // Return if no class has been instrumented. + val startTimestamp = startTimestamp ?: return + + // Update the list of covered IDs with the coverage information for the current run. + updateCoveredIdsWithCoverageMap() + + val dumpTimestamp = Instant.now() + val outWriter = ExecutionDataWriter(outStream) + outWriter.visitSessionInfo( + SessionInfo(UUID.randomUUID().toString(), startTimestamp.epochSecond, dumpTimestamp.epochSecond), + ) + analyzeJacocoCoverage(coveredIds.toSet()).accept(outWriter) + } + + /** + * Build up a JaCoCo [ExecutionDataStore] based on [coveredIds] containing the internally gathered coverage information. + */ + private fun analyzeJacocoCoverage(coveredIds: Set<Int>): ExecutionDataStore { + val executionDataStore = ExecutionDataStore() + val sortedCoveredIds = (additionalCoverage + coveredIds).sorted().toIntArray() + for ((internalClassName, info) in instrumentedClassInfo) { + // Determine the subarray of coverage IDs in sortedCoveredIds that contains the IDs generated while + // instrumenting the current class. Since the ID array is sorted, use binary search. + var coveredIdsStart = sortedCoveredIds.binarySearch(info.initialEdgeId) + if (coveredIdsStart < 0) { + coveredIdsStart = -(coveredIdsStart + 1) + } + var coveredIdsEnd = sortedCoveredIds.binarySearch(info.nextEdgeId) + if (coveredIdsEnd < 0) { + coveredIdsEnd = -(coveredIdsEnd + 1) + } + if (coveredIdsStart == coveredIdsEnd) { + // No coverage data for the class. + continue + } + check(coveredIdsStart in 0 until coveredIdsEnd && coveredIdsEnd <= sortedCoveredIds.size) { + "Invalid range [$coveredIdsStart, $coveredIdsEnd) with coveredIds.size=${sortedCoveredIds.size}" + } + // Generate a probes array for the current class only, i.e., mapping info.initialEdgeId to 0. + val probes = BooleanArray(info.nextEdgeId - info.initialEdgeId) + (coveredIdsStart until coveredIdsEnd).asSequence() + .map { + val globalEdgeId = sortedCoveredIds[it] + globalEdgeId - info.initialEdgeId + } + .forEach { classLocalEdgeId -> + probes[classLocalEdgeId] = true + } + executionDataStore.visitClassExecution(ExecutionData(info.classId, internalClassName, probes)) + } + return executionDataStore + } + + /** + * Create a [CoverageBuilder] containing all classes matching the include/exclude pattern and their coverage statistics. + */ + fun analyzeCoverage(coveredIds: Set<Int>): CoverageBuilder? { + return try { + val coverage = CoverageBuilder() + analyzeAllUncoveredClasses(coverage) + val executionDataStore = analyzeJacocoCoverage(coveredIds) + for ((internalClassName, info) in instrumentedClassInfo) { + EdgeCoverageInstrumentor(ClassInstrumentor.defaultEdgeCoverageStrategy, ClassInstrumentor.defaultCoverageMap, 0) + .analyze( + executionDataStore, + coverage, + info.bytecode, + internalClassName, + ) + } + coverage + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + /** + * Traverses the entire classpath and analyzes all uncovered classes that match the include/exclude pattern. + * The returned [CoverageBuilder] will report coverage information for *all* classes on the classpath, not just + * those that were loaded while the fuzzer ran. + */ + private fun analyzeAllUncoveredClasses(coverage: CoverageBuilder): CoverageBuilder { + val coveredClassNames = instrumentedClassInfo + .keys + .asSequence() + .map { it.replace('/', '.') } + .toSet() + ClassGraph() + .enableClassInfo() + .ignoreClassVisibility() + .rejectPackages( + // Always exclude Jazzer-internal packages (including ClassGraph itself) from coverage reports. Classes + // from the Java standard library are never traversed. + "com.code_intelligence.jazzer.*", + "jaz", + ) + .scan().use { result -> + // ExecutionDataStore is used to look up existing coverage during analysis of the class files, + // no entries are added during that. Passing in an empty store is fine for uncovered files. + val emptyExecutionDataStore = ExecutionDataStore() + result.allClasses + .asSequence() + .filter { classInfo -> classNameGlobber.includes(classInfo.name) } + .filterNot { classInfo -> classInfo.name in coveredClassNames } + .forEach { classInfo -> + classInfo.resource.use { resource -> + EdgeCoverageInstrumentor(ClassInstrumentor.defaultEdgeCoverageStrategy, ClassInstrumentor.defaultCoverageMap, 0).analyze( + emptyExecutionDataStore, + coverage, + resource.load(), + classInfo.name.replace('.', '/'), + ) + } + } + } + return coverage + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt new file mode 100644 index 00000000..9d02c04f --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt @@ -0,0 +1,105 @@ +// Copyright 2021 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.instrumentor + +import org.objectweb.asm.Type +import java.lang.reflect.Constructor +import java.lang.reflect.Executable +import java.lang.reflect.Method + +val Class<*>.descriptor: String + get() = Type.getDescriptor(this) + +val Executable.descriptor: String + get() = if (this is Method) { + Type.getMethodDescriptor(this) + } else { + Type.getConstructorDescriptor(this as Constructor<*>?) + } + +internal fun isPrimitiveType(typeDescriptor: String): Boolean { + return typeDescriptor in arrayOf("B", "C", "D", "F", "I", "J", "S", "V", "Z") +} + +private fun isPrimitiveType(typeDescriptor: Char) = isPrimitiveType(typeDescriptor.toString()) + +internal fun getWrapperTypeDescriptor(typeDescriptor: String): String = when (typeDescriptor) { + "B" -> "Ljava/lang/Byte;" + "C" -> "Ljava/lang/Character;" + "D" -> "Ljava/lang/Double;" + "F" -> "Ljava/lang/Float;" + "I" -> "Ljava/lang/Integer;" + "J" -> "Ljava/lang/Long;" + "S" -> "Ljava/lang/Short;" + "V" -> "Ljava/lang/Void;" + "Z" -> "Ljava/lang/Boolean;" + else -> typeDescriptor +} + +// Removes the 'L' and ';' prefix/suffix from signatures to get the full class name. +// Note that array signatures '[Ljava/lang/String;' already have the correct form. +internal fun extractInternalClassName(typeDescriptor: String): String { + return if (typeDescriptor.startsWith("L") && typeDescriptor.endsWith(";")) { + typeDescriptor.substring(1, typeDescriptor.length - 1) + } else { + typeDescriptor + } +} + +internal fun extractParameterTypeDescriptors(methodDescriptor: String): List<String> { + require(methodDescriptor.startsWith('(')) { "Method descriptor must start with '('" } + val endOfParameterPart = methodDescriptor.indexOf(')') - 1 + require(endOfParameterPart >= 0) { "Method descriptor must contain ')'" } + var remainingDescriptorList = methodDescriptor.substring(1..endOfParameterPart) + val parameterDescriptors = mutableListOf<String>() + while (remainingDescriptorList.isNotEmpty()) { + val nextDescriptor = extractNextTypeDescriptor(remainingDescriptorList) + parameterDescriptors.add(nextDescriptor) + remainingDescriptorList = remainingDescriptorList.removePrefix(nextDescriptor) + } + return parameterDescriptors +} + +internal fun extractReturnTypeDescriptor(methodDescriptor: String): String { + require(methodDescriptor.startsWith('(')) { "Method descriptor must start with '('" } + val endBracketPos = methodDescriptor.indexOf(')') + require(endBracketPos >= 0) { "Method descriptor must contain ')'" } + val startOfReturnValue = endBracketPos + 1 + return extractNextTypeDescriptor(methodDescriptor.substring(startOfReturnValue)) +} + +private fun extractNextTypeDescriptor(input: String): String { + require(input.isNotEmpty()) { "Type descriptor must not be empty" } + // Skip over arbitrarily many '[' to support multi-dimensional arrays. + val firstNonArrayPrefixCharPos = input.indexOfFirst { it != '[' } + require(firstNonArrayPrefixCharPos >= 0) { "Array descriptor must contain type" } + val firstTypeChar = input[firstNonArrayPrefixCharPos] + return when { + // Primitive type + isPrimitiveType(firstTypeChar) -> { + input.substring(0..firstNonArrayPrefixCharPos) + } + // Object type + firstTypeChar == 'L' -> { + val endOfClassNamePos = input.indexOf(';') + require(endOfClassNamePos > 0) { "Class type indicated by L must end with ;" } + input.substring(0..endOfClassNamePos) + } + // Invalid type + else -> { + throw IllegalArgumentException("Invalid type: $firstTypeChar") + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt new file mode 100644 index 00000000..d4210dc4 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt @@ -0,0 +1,35 @@ +// Copyright 2021 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.instrumentor + +import java.security.MessageDigest +import java.security.SecureRandom + +// This RNG is resistant to collisions (even under XOR) but fully deterministic. +internal class DeterministicRandom(vararg contexts: String) { + private val random = SecureRandom.getInstance("SHA1PRNG").apply { + val contextHash = MessageDigest.getInstance("SHA-256").run { + for (context in contexts) { + update(context.toByteArray()) + } + digest() + } + setSeed(contextHash) + } + + fun nextInt(bound: Int) = random.nextInt(bound) + + fun nextInt() = random.nextInt() +} diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt new file mode 100644 index 00000000..975f3987 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt @@ -0,0 +1,187 @@ +// Copyright 2021 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.instrumentor + +import com.code_intelligence.jazzer.third_party.org.jacoco.core.analysis.Analyzer +import com.code_intelligence.jazzer.third_party.org.jacoco.core.analysis.ICoverageVisitor +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataStore +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.flow.ClassProbesAdapter +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.flow.ClassProbesVisitor +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.flow.IClassProbesAdapterFactory +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.ClassInstrumenter +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.IProbeArrayStrategy +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.IProbeInserterFactory +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.InstrSupport +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.ProbeInserter +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.MethodVisitor +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles.publicLookup +import java.lang.invoke.MethodType.methodType +import kotlin.math.max + +/** + * A particular way to instrument bytecode for edge coverage using a coverage map class available to + * hold the collected coverage data at runtime. + */ +interface EdgeCoverageStrategy { + + /** + * Inject bytecode instrumentation on a control flow edge with ID [edgeId], with access to the + * local variable [variable] that is populated at the beginning of each method by the + * instrumentation injected in [loadLocalVariable]. + */ + fun instrumentControlFlowEdge( + mv: MethodVisitor, + edgeId: Int, + variable: Int, + coverageMapInternalClassName: String, + ) + + /** + * The maximal number of stack elements used by [instrumentControlFlowEdge]. + */ + val instrumentControlFlowEdgeStackSize: Int + + /** + * The type of the local variable used by the instrumentation in the format used by + * [MethodVisitor.visitFrame]'s `local` parameter, or `null` if the instrumentation does not use + * one. + * @see https://asm.ow2.io/javadoc/org/objectweb/asm/MethodVisitor.html#visitFrame(int,int,java.lang.Object%5B%5D,int,java.lang.Object%5B%5D) + */ + val localVariableType: Any? + + /** + * Inject bytecode that loads the coverage counters of the coverage map class described by + * [coverageMapInternalClassName] into the local variable [variable]. + */ + fun loadLocalVariable(mv: MethodVisitor, variable: Int, coverageMapInternalClassName: String) + + /** + * The maximal number of stack elements used by [loadLocalVariable]. + */ + val loadLocalVariableStackSize: Int +} + +// An instance of EdgeCoverageInstrumentor should only be used to instrument a single class as it +// internally tracks the edge IDs, which have to be globally unique. +class EdgeCoverageInstrumentor( + private val strategy: EdgeCoverageStrategy, + /** + * The class must have the following public static member + * - method enlargeIfNeeded(int nextEdgeId): Called before a new edge ID is emitted. + */ + coverageMapClass: Class<*>, + private val initialEdgeId: Int, +) : Instrumentor { + private var nextEdgeId = initialEdgeId + + private val coverageMapInternalClassName = coverageMapClass.name.replace('.', '/') + private val enlargeIfNeeded: MethodHandle = + publicLookup().findStatic( + coverageMapClass, + "enlargeIfNeeded", + methodType( + Void::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + ), + ) + + override fun instrument(internalClassName: String, bytecode: ByteArray): ByteArray { + val reader = InstrSupport.classReaderFor(bytecode) + val writer = ClassWriter(reader, 0) + val version = InstrSupport.getMajorVersion(reader) + val visitor = EdgeCoverageClassProbesAdapter( + ClassInstrumenter(edgeCoverageProbeArrayStrategy, edgeCoverageProbeInserterFactory, writer), + InstrSupport.needsFrames(version), + ) + reader.accept(visitor, ClassReader.EXPAND_FRAMES) + return writer.toByteArray() + } + + fun analyze(executionData: ExecutionDataStore, coverageVisitor: ICoverageVisitor, bytecode: ByteArray, internalClassName: String) { + Analyzer(executionData, coverageVisitor, edgeCoverageClassProbesAdapterFactory).run { + analyzeClass(bytecode, internalClassName) + } + } + + val numEdges + get() = nextEdgeId - initialEdgeId + + private fun nextEdgeId(): Int { + enlargeIfNeeded.invokeExact(nextEdgeId) + return nextEdgeId++ + } + + /** + * A [ProbeInserter] that injects bytecode instrumentation at every control flow edge and + * modifies the stack size and number of local variables accordingly. + */ + private inner class EdgeCoverageProbeInserter( + access: Int, + name: String, + desc: String, + mv: MethodVisitor, + arrayStrategy: IProbeArrayStrategy, + ) : ProbeInserter(access, name, desc, mv, arrayStrategy) { + override fun insertProbe(id: Int) { + strategy.instrumentControlFlowEdge(mv, id, variable, coverageMapInternalClassName) + } + + override fun visitMaxs(maxStack: Int, maxLocals: Int) { + val newMaxStack = max(maxStack + strategy.instrumentControlFlowEdgeStackSize, strategy.loadLocalVariableStackSize) + val newMaxLocals = maxLocals + if (strategy.localVariableType != null) 1 else 0 + mv.visitMaxs(newMaxStack, newMaxLocals) + } + + override fun getLocalVariableType() = strategy.localVariableType + } + + private val edgeCoverageProbeInserterFactory = + IProbeInserterFactory { access, name, desc, mv, arrayStrategy -> + EdgeCoverageProbeInserter(access, name, desc, mv, arrayStrategy) + } + + private inner class EdgeCoverageClassProbesAdapter(private val cpv: ClassProbesVisitor, trackFrames: Boolean) : + ClassProbesAdapter(cpv, trackFrames) { + override fun nextId(): Int = nextEdgeId() + + override fun visitEnd() { + cpv.visitTotalProbeCount(numEdges) + // Avoid calling super.visitEnd() as that invokes cpv.visitTotalProbeCount with an + // incorrect value of `count`. + cpv.visitEnd() + } + } + + private val edgeCoverageClassProbesAdapterFactory = IClassProbesAdapterFactory { probesVisitor, trackFrames -> + EdgeCoverageClassProbesAdapter(probesVisitor, trackFrames) + } + + private val edgeCoverageProbeArrayStrategy = object : IProbeArrayStrategy { + override fun storeInstance(mv: MethodVisitor, clinit: Boolean, variable: Int): Int { + strategy.loadLocalVariable(mv, variable, coverageMapInternalClassName) + return strategy.loadLocalVariableStackSize + } + + override fun addMembers(cv: ClassVisitor, probeCount: Int) {} + } +} + +fun MethodVisitor.push(value: Int) { + InstrSupport.push(this, value) +} diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt new file mode 100644 index 00000000..077ab10e --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt @@ -0,0 +1,132 @@ +// Copyright 2021 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. + +@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + +package com.code_intelligence.jazzer.instrumentor + +import com.code_intelligence.jazzer.api.HookType +import com.code_intelligence.jazzer.api.MethodHook +import java.lang.invoke.MethodHandle +import java.lang.reflect.Method +import java.lang.reflect.Modifier + +class Hook private constructor( + private val targetClassName: String, + val hookType: HookType, + val targetMethodName: String, + val targetMethodDescriptor: String?, + val additionalClassesToHook: List<String>, + val targetInternalClassName: String, + private val targetReturnTypeDescriptor: String?, + private val targetWrappedReturnTypeDescriptor: String?, + private val hookClassName: String, + val hookInternalClassName: String, + val hookMethodName: String, + val hookMethodDescriptor: String, +) { + + override fun toString(): String { + return "$hookType $targetClassName.$targetMethodName: $hookClassName.$hookMethodName $additionalClassesToHook" + } + + companion object { + fun createAndVerifyHook(hookMethod: Method, hookData: MethodHook, className: String): Hook { + return createHook(hookMethod, hookData, className).also { + verify(hookMethod, it) + } + } + + private fun createHook(hookMethod: Method, annotation: MethodHook, targetClassName: String): Hook { + val targetReturnTypeDescriptor = annotation.targetMethodDescriptor + .takeIf { it.isNotBlank() }?.let { extractReturnTypeDescriptor(it) } + val hookClassName: String = hookMethod.declaringClass.name + return Hook( + targetClassName = targetClassName, + hookType = annotation.type, + targetMethodName = annotation.targetMethod, + targetMethodDescriptor = annotation.targetMethodDescriptor.takeIf { it.isNotBlank() }, + additionalClassesToHook = annotation.additionalClassesToHook.asList(), + targetInternalClassName = targetClassName.replace('.', '/'), + targetReturnTypeDescriptor = targetReturnTypeDescriptor, + targetWrappedReturnTypeDescriptor = targetReturnTypeDescriptor?.let { getWrapperTypeDescriptor(it) }, + hookClassName = hookClassName, + hookInternalClassName = hookClassName.replace('.', '/'), + hookMethodName = hookMethod.name, + hookMethodDescriptor = hookMethod.descriptor, + ) + } + + private fun verify(hookMethod: Method, potentialHook: Hook) { + // Verify the hook method's modifiers (public static). + require(Modifier.isPublic(hookMethod.modifiers)) { "$potentialHook: hook method must be public" } + require(Modifier.isStatic(hookMethod.modifiers)) { "$potentialHook: hook method must be static" } + + // Verify the hook method's parameter count. + val numParameters = hookMethod.parameters.size + when (potentialHook.hookType) { + HookType.BEFORE, HookType.REPLACE -> require(numParameters == 4) { "$potentialHook: incorrect number of parameters (expected 4)" } + HookType.AFTER -> require(numParameters == 5) { "$potentialHook: incorrect number of parameters (expected 5)" } + } + + // Verify the hook method's parameter types. + val parameterTypes = hookMethod.parameterTypes + require(parameterTypes[0] == MethodHandle::class.java) { "$potentialHook: first parameter must have type MethodHandle" } + require(parameterTypes[1] == Object::class.java || parameterTypes[1].name == potentialHook.targetClassName) { "$potentialHook: second parameter must have type Object or ${potentialHook.targetClassName}" } + require(parameterTypes[2] == Array<Object>::class.java) { "$potentialHook: third parameter must have type Object[]" } + require(parameterTypes[3] == Int::class.javaPrimitiveType) { "$potentialHook: fourth parameter must have type int" } + + // Verify the hook method's return type if possible. + when (potentialHook.hookType) { + HookType.BEFORE, HookType.AFTER -> require(hookMethod.returnType == Void.TYPE) { + "$potentialHook: return type must be void" + } + HookType.REPLACE -> if (potentialHook.targetReturnTypeDescriptor != null) { + if (potentialHook.targetMethodName == "<init>") { + require(hookMethod.returnType.name == potentialHook.targetClassName) { "$potentialHook: return type must be ${potentialHook.targetClassName} to match target constructor" } + } else if (potentialHook.targetReturnTypeDescriptor == "V") { + require(hookMethod.returnType.descriptor == "V") { "$potentialHook: return type must be void" } + } else { + require( + hookMethod.returnType.descriptor in listOf( + java.lang.Object::class.java.descriptor, + potentialHook.targetReturnTypeDescriptor, + potentialHook.targetWrappedReturnTypeDescriptor, + ), + ) { + "$potentialHook: return type must have type Object or match the descriptors ${potentialHook.targetReturnTypeDescriptor} or ${potentialHook.targetWrappedReturnTypeDescriptor}" + } + } + } + } + + // AfterMethodHook only: Verify the type of the last parameter if known. Even if not + // known, it must not be a primitive value. + if (potentialHook.hookType == HookType.AFTER) { + if (potentialHook.targetReturnTypeDescriptor != null) { + require( + parameterTypes[4] == java.lang.Object::class.java || + parameterTypes[4].descriptor == potentialHook.targetWrappedReturnTypeDescriptor, + ) { + "$potentialHook: fifth parameter must have type Object or match the descriptor ${potentialHook.targetWrappedReturnTypeDescriptor}" + } + } else { + require(!parameterTypes[4].isPrimitive) { + "$potentialHook: fifth parameter must not be a primitive type, use a boxed type instead" + } + } + } + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt new file mode 100644 index 00000000..3c0d97c9 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt @@ -0,0 +1,63 @@ +// Copyright 2021 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.instrumentor + +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.MethodVisitor + +internal class HookInstrumentor( + private val hooks: Iterable<Hook>, + private val java6Mode: Boolean, + private val classWithHooksEnabledField: String?, +) : Instrumentor { + + private lateinit var random: DeterministicRandom + + override fun instrument(internalClassName: String, bytecode: ByteArray): ByteArray { + val reader = ClassReader(bytecode) + val writer = ClassWriter(reader, ClassWriter.COMPUTE_MAXS) + random = DeterministicRandom("hook", reader.className) + val interceptor = object : ClassVisitor(Instrumentor.ASM_API_VERSION, writer) { + override fun visitMethod( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + exceptions: Array<String>?, + ): MethodVisitor? { + val mv = cv.visitMethod(access, name, descriptor, signature, exceptions) ?: return null + return if (shouldInstrument(access)) { + makeHookMethodVisitor( + internalClassName, + access, + name, + descriptor, + mv, + hooks, + java6Mode, + random, + classWithHooksEnabledField, + ) + } else { + mv + } + } + } + reader.accept(interceptor, ClassReader.EXPAND_FRAMES) + return writer.toByteArray() + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt new file mode 100644 index 00000000..f5118fd6 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt @@ -0,0 +1,513 @@ +// Copyright 2021 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.instrumentor + +import com.code_intelligence.jazzer.api.HookType +import org.objectweb.asm.Handle +import org.objectweb.asm.Label +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.commons.AnalyzerAdapter +import org.objectweb.asm.commons.LocalVariablesSorter +import java.util.concurrent.atomic.AtomicBoolean + +internal fun makeHookMethodVisitor( + owner: String, + access: Int, + name: String?, + descriptor: String?, + methodVisitor: MethodVisitor?, + hooks: Iterable<Hook>, + java6Mode: Boolean, + random: DeterministicRandom, + classWithHooksEnabledField: String?, +): MethodVisitor { + return HookMethodVisitor( + owner, + access, + name, + descriptor, + methodVisitor, + hooks, + java6Mode, + random, + classWithHooksEnabledField, + ).lvs +} + +private class HookMethodVisitor( + owner: String, + access: Int, + val name: String?, + descriptor: String?, + methodVisitor: MethodVisitor?, + hooks: Iterable<Hook>, + private val java6Mode: Boolean, + private val random: DeterministicRandom, + private val classWithHooksEnabledField: String?, +) : MethodVisitor( + Instrumentor.ASM_API_VERSION, + // AnalyzerAdapter computes stack map frames at every instruction, which is needed for the + // conditional hook logic as it adds a conditional jump. Before Java 7, stack map frames were + // neither included nor required in class files. + // + // Note: Delegating to AnalyzerAdapter rather than having AnalyzerAdapter delegate to our + // MethodVisitor is unusual. We do this since we insert conditional jumps around method calls, + // which requires knowing the stack map both before and after the call. If AnalyzerAdapter + // delegated to this MethodVisitor, we would only be able to access the stack map before the + // method call in visitMethodInsn. + if (classWithHooksEnabledField != null && !java6Mode) { + AnalyzerAdapter( + owner, + access, + name, + descriptor, + methodVisitor, + ) + } else { + methodVisitor + }, +) { + + companion object { + private val showUnsupportedHookWarning = AtomicBoolean(true) + } + + val lvs = object : LocalVariablesSorter(Instrumentor.ASM_API_VERSION, access, descriptor, this) { + override fun updateNewLocals(newLocals: Array<Any>) { + // The local variables involved in calling hooks do not need to outlive the current + // basic block and should thus not appear in stack map frames. By requesting the + // LocalVariableSorter to fill their entries in stack map frames with TOP, they will + // be treated like an unused local variable slot. + newLocals.fill(Opcodes.TOP) + } + } + + private val hooks = hooks.groupBy { hook -> + var hookKey = "${hook.hookType}#${hook.targetInternalClassName}#${hook.targetMethodName}" + if (hook.targetMethodDescriptor != null) { + hookKey += "#${hook.targetMethodDescriptor}" + } + hookKey + } + + override fun visitMethodInsn( + opcode: Int, + owner: String, + methodName: String, + methodDescriptor: String, + isInterface: Boolean, + ) { + if (!isMethodInvocationOp(opcode)) { + mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) + return + } + handleMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) + } + + // Transforms a stack map specification from the form used by the JVM and AnalyzerAdapter, where + // LONG and DOUBLE values are followed by an additional TOP entry, to the form accepted by + // visitFrame, which doesn't expect this additional entry. + private fun dropImplicitTop(stack: Collection<Any>?): Array<Any>? { + if (stack == null) { + return null + } + val filteredStack = mutableListOf<Any>() + var previousElement: Any? = null + for (element in stack) { + if (element != Opcodes.TOP || (previousElement != Opcodes.DOUBLE && previousElement != Opcodes.LONG)) { + filteredStack.add(element) + } + previousElement = element + } + return filteredStack.toTypedArray() + } + + private fun storeFrame(aa: AnalyzerAdapter?): Pair<Array<Any>?, Array<Any>?>? { + return Pair(dropImplicitTop((aa ?: return null).locals), dropImplicitTop(aa.stack)) + } + + fun handleMethodInsn( + opcode: Int, + owner: String, + methodName: String, + methodDescriptor: String, + isInterface: Boolean, + ) { + val matchingHooks = findMatchingHooks(owner, methodName, methodDescriptor) + + if (matchingHooks.isEmpty()) { + mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) + return + } + + val skipHooksLabel = Label() + val applyHooksLabel = Label() + val useConditionalHooks = classWithHooksEnabledField != null + var postCallFrame: Pair<Array<Any>?, Array<Any>?>? = null + if (useConditionalHooks) { + val preCallFrame = (mv as? AnalyzerAdapter)?.let { storeFrame(it) } + // If hooks aren't enabled, skip the hook invocations. + mv.visitFieldInsn( + Opcodes.GETSTATIC, + classWithHooksEnabledField, + "hooksEnabled", + "Z", + ) + mv.visitJumpInsn(Opcodes.IFNE, applyHooksLabel) + mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) + postCallFrame = (mv as? AnalyzerAdapter)?.let { storeFrame(it) } + mv.visitJumpInsn(Opcodes.GOTO, skipHooksLabel) + // Needs a stack map frame as both the successor of an unconditional jump and the target + // of a jump. + mv.visitLabel(applyHooksLabel) + if (preCallFrame != null) { + mv.visitFrame( + Opcodes.F_NEW, + preCallFrame.first?.size ?: 0, + preCallFrame.first, + preCallFrame.second?.size ?: 0, + preCallFrame.second, + ) + } + // All successor instructions emitted below do not have a stack map frame attached, so + // we do not need to emit a NOP to prevent duplicated stack map frames. + } + + val paramDescriptors = extractParameterTypeDescriptors(methodDescriptor) + val localObjArr = storeMethodArguments(paramDescriptors) + // If the method we're hooking is not static there is now a reference to + // the object the method was invoked on at the top of the stack. + // If the method is static, that object is missing. We make up for it by pushing a null ref. + if (opcode == Opcodes.INVOKESTATIC) { + mv.visitInsn(Opcodes.ACONST_NULL) + } + + // Save the owner object to a new local variable + val ownerDescriptor = "L$owner;" + val localOwnerObj = lvs.newLocal(Type.getType(ownerDescriptor)) + mv.visitVarInsn(Opcodes.ASTORE, localOwnerObj) // consume objectref + // We now removed all values for the original method call from the operand stack + // and saved them to local variables. + + val returnTypeDescriptor = extractReturnTypeDescriptor(methodDescriptor) + // Create a local variable to store the return value + val localReturnObj = lvs.newLocal(Type.getType(getWrapperTypeDescriptor(returnTypeDescriptor))) + + matchingHooks.forEachIndexed { index, hook -> + // The hookId is used to identify a call site. + val hookId = random.nextInt() + + // Start to build the arguments for the hook method. + if (methodName == "<init>") { + // Constructor is invoked on an uninitialized object, and that's still on the stack. + // In case of REPLACE pop it from the stack and replace it afterwards with the returned + // one from the hook. + if (hook.hookType == HookType.REPLACE) { + mv.visitInsn(Opcodes.POP) + } + // Special case for constructors: + // We cannot create a MethodHandle for a constructor, so we push null instead. + mv.visitInsn(Opcodes.ACONST_NULL) // push nullref + // Only pass the this object if it has been initialized by the time the hook is invoked. + if (hook.hookType == HookType.AFTER) { + mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) + } else { + mv.visitInsn(Opcodes.ACONST_NULL) // push nullref + } + } else { + // Push a MethodHandle representing the hooked method. + val handleOpcode = when (opcode) { + Opcodes.INVOKEVIRTUAL -> Opcodes.H_INVOKEVIRTUAL + Opcodes.INVOKEINTERFACE -> Opcodes.H_INVOKEINTERFACE + Opcodes.INVOKESTATIC -> Opcodes.H_INVOKESTATIC + Opcodes.INVOKESPECIAL -> Opcodes.H_INVOKESPECIAL + else -> -1 + } + if (java6Mode) { + // MethodHandle constants (type 15) are not supported in Java 6 class files (major version 50). + mv.visitInsn(Opcodes.ACONST_NULL) // push nullref + } else { + mv.visitLdcInsn( + Handle( + handleOpcode, + owner, + methodName, + methodDescriptor, + isInterface, + ), + ) // push MethodHandle + } + // Stack layout: ... | MethodHandle (objectref) + // Push the owner object again + mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) + } + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) + // Push a reference to our object array with the saved arguments + mv.visitVarInsn(Opcodes.ALOAD, localObjArr) + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) + // Push the hook id + mv.visitLdcInsn(hookId) + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) + // How we proceed depends on the type of hook we want to implement + when (hook.hookType) { + HookType.BEFORE -> { + // Call the hook method + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + hook.hookInternalClassName, + hook.hookMethodName, + hook.hookMethodDescriptor, + false, + ) + + // Call the original method if this is the last BEFORE hook. If not, the original method will be + // called by the next AFTER hook. + if (index == matchingHooks.lastIndex) { + // Stack layout: ... + // Push the values for the original method call onto the stack again + if (opcode != Opcodes.INVOKESTATIC) { + mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object + } + loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments + // Stack layout: ... | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ... + mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) + } + } + + HookType.REPLACE -> { + // Call the hook method + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + hook.hookInternalClassName, + hook.hookMethodName, + hook.hookMethodDescriptor, + false, + ) + // Stack layout: ... | [return value (primitive/objectref)] + // Check if we need to process the return value + if (returnTypeDescriptor != "V") { + val hookMethodReturnType = extractReturnTypeDescriptor(hook.hookMethodDescriptor) + // if the hook method's return type is primitive we don't need to unwrap or cast it + if (!isPrimitiveType(hookMethodReturnType)) { + // Check if the returned object type is different than the one that should be returned + // If a primitive should be returned we check it's wrapper type + val expectedType = getWrapperTypeDescriptor(returnTypeDescriptor) + if (expectedType != hookMethodReturnType) { + // Cast object + mv.visitTypeInsn(Opcodes.CHECKCAST, extractInternalClassName(expectedType)) + } + // Check if we need to unwrap the returned object + unwrapTypeIfPrimitive(returnTypeDescriptor) + } + } + } + + HookType.AFTER -> { + // Call the original method before the first AFTER hook + if (index == 0 || matchingHooks[index - 1].hookType != HookType.AFTER) { + // Push the values for the original method call again onto the stack + if (opcode != Opcodes.INVOKESTATIC) { + mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object + } + loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) + // | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ... + mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) + if (returnTypeDescriptor == "V") { + // If the method didn't return anything, we push a nullref as placeholder + mv.visitInsn(Opcodes.ACONST_NULL) // push nullref + } + // Wrap return value if it is a primitive type + wrapTypeIfPrimitive(returnTypeDescriptor) + mv.visitVarInsn(Opcodes.ASTORE, localReturnObj) // consume objectref + } + mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref + + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) + // | return value (objectref) + // Store the result value in a local variable (but keep it on the stack) + // Call the hook method + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + hook.hookInternalClassName, + hook.hookMethodName, + hook.hookMethodDescriptor, + false, + ) + // Stack layout: ... + // Push the return value on the stack after the last AFTER hook if the original method returns a value + if (index == matchingHooks.size - 1 && returnTypeDescriptor != "V") { + // Push the return value again + mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref + // Unwrap it, if it was a primitive value + unwrapTypeIfPrimitive(returnTypeDescriptor) + // Stack layout: ... | return value (primitive/objectref) + } + } + } + } + if (useConditionalHooks) { + // Needs a stack map frame as the target of a jump. + mv.visitLabel(skipHooksLabel) + if (postCallFrame != null) { + mv.visitFrame( + Opcodes.F_NEW, + postCallFrame.first?.size ?: 0, + postCallFrame.first, + postCallFrame.second?.size ?: 0, + postCallFrame.second, + ) + } + // We do not control the next visitor calls, but we must not emit two frames for the + // same instruction. + mv.visitInsn(Opcodes.NOP) + } + } + + private fun isMethodInvocationOp(opcode: Int) = opcode in listOf( + Opcodes.INVOKEVIRTUAL, + Opcodes.INVOKEINTERFACE, + Opcodes.INVOKESTATIC, + Opcodes.INVOKESPECIAL, + ) + + private fun findMatchingHooks(owner: String, name: String, descriptor: String): List<Hook> { + val result = HookType.values().flatMap { hookType -> + val withoutDescriptorKey = "$hookType#$owner#$name" + val withDescriptorKey = "$withoutDescriptorKey#$descriptor" + hooks[withDescriptorKey].orEmpty() + hooks[withoutDescriptorKey].orEmpty() + }.sortedBy { it.hookType } + val replaceHookCount = result.count { it.hookType == HookType.REPLACE } + check( + replaceHookCount == 0 || + (replaceHookCount == 1 && result.size == 1), + ) { + "For a given method, You can either have a single REPLACE hook or BEFORE/AFTER hooks. Found:\n $result" + } + + return result + .filter { !isReplaceHookInJava6mode(it) } + .sortedByDescending { it.toString() } + } + + private fun isReplaceHookInJava6mode(hook: Hook): Boolean { + if (java6Mode && hook.hookType == HookType.REPLACE) { + if (showUnsupportedHookWarning.getAndSet(false)) { + println( + """WARN: Some hooks could not be applied to class files built for Java 7 or lower. + |WARN: Ensure that the fuzz target and its dependencies are compiled with + |WARN: -target 8 or higher to identify as many bugs as possible. + """.trimMargin(), + ) + } + return true + } + return false + } + + // Stores all arguments for a method call in a local object array. + // paramDescriptors: The type descriptors for all method arguments + private fun storeMethodArguments(paramDescriptors: List<String>): Int { + // Allocate a new Object[] for the methods parameters. + mv.visitIntInsn(Opcodes.SIPUSH, paramDescriptors.size) + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object") + val localObjArr = lvs.newLocal(Type.getType("[Ljava/lang/Object;")) + mv.visitVarInsn(Opcodes.ASTORE, localObjArr) + + // Loop over all arguments in reverse order (because the last argument is on top). + for ((argIdx, argDescriptor) in paramDescriptors.withIndex().reversed()) { + // If the argument is a primitive type, wrap it in it's wrapper class + wrapTypeIfPrimitive(argDescriptor) + // Store the argument in our object array, for that we need to shape the stack first. + // Stack layout: ... | method argument (objectref) + mv.visitVarInsn(Opcodes.ALOAD, localObjArr) + // Stack layout: ... | method argument (objectref) | object array (arrayref) + mv.visitInsn(Opcodes.SWAP) + // Stack layout: ... | object array (arrayref) | method argument (objectref) + mv.visitIntInsn(Opcodes.SIPUSH, argIdx) + // Stack layout: ... | object array (arrayref) | method argument (objectref) | argument index (int) + mv.visitInsn(Opcodes.SWAP) + // Stack layout: ... | object array (arrayref) | argument index (int) | method argument (objectref) + mv.visitInsn(Opcodes.AASTORE) // consume all three: arrayref, index, value + // Stack layout: ... + // Continue with the remaining method arguments + } + + // Return a reference to the array with the parameters. + return localObjArr + } + + // Loads all arguments for a method call from a local object array. + // argTypeSigs: The type signatures for all method arguments + // localObjArr: Index of a local variable containing an object array where the arguments will be loaded from + private fun loadMethodArguments(paramDescriptors: List<String>, localObjArr: Int) { + // Loop over all arguments + for ((argIdx, argDescriptor) in paramDescriptors.withIndex()) { + // Push a reference to the object array on the stack + mv.visitVarInsn(Opcodes.ALOAD, localObjArr) + // Stack layout: ... | object array (arrayref) + // Push the index of the current argument on the stack + mv.visitIntInsn(Opcodes.SIPUSH, argIdx) + // Stack layout: ... | object array (arrayref) | argument index (int) + // Load the argument from the array + mv.visitInsn(Opcodes.AALOAD) + // Stack layout: ... | method argument (objectref) + // Cast object to it's original type (or it's wrapper object) + val wrapperTypeDescriptor = getWrapperTypeDescriptor(argDescriptor) + mv.visitTypeInsn(Opcodes.CHECKCAST, extractInternalClassName(wrapperTypeDescriptor)) + // If the argument is a supposed to be a primitive type, unwrap the wrapped type + unwrapTypeIfPrimitive(argDescriptor) + // Stack layout: ... | method argument (primitive/objectref) + // Continue with the remaining method arguments + } + } + + // Removes a primitive value from the top of the operand stack + // and pushes it enclosed in its wrapper type (e.g. removes int, pushes Integer). + // This is done by calling .valueOf(...) on the wrapper class. + private fun wrapTypeIfPrimitive(unwrappedTypeDescriptor: String) { + if (!isPrimitiveType(unwrappedTypeDescriptor) || unwrappedTypeDescriptor == "V") return + val wrapperTypeDescriptor = getWrapperTypeDescriptor(unwrappedTypeDescriptor) + val wrapperType = extractInternalClassName(wrapperTypeDescriptor) + val valueOfDescriptor = "($unwrappedTypeDescriptor)$wrapperTypeDescriptor" + mv.visitMethodInsn(Opcodes.INVOKESTATIC, wrapperType, "valueOf", valueOfDescriptor, false) + } + + // Removes a wrapper object around a given primitive type from the top of the operand stack + // and pushes the primitive value it contains (e.g. removes Integer, pushes int). + // This is done by calling .intValue(...) / .charValue(...) / ... on the wrapper object. + private fun unwrapTypeIfPrimitive(primitiveTypeDescriptor: String) { + val (methodName, wrappedTypeDescriptor) = when (primitiveTypeDescriptor) { + "B" -> Pair("byteValue", "java/lang/Byte") + "C" -> Pair("charValue", "java/lang/Character") + "D" -> Pair("doubleValue", "java/lang/Double") + "F" -> Pair("floatValue", "java/lang/Float") + "I" -> Pair("intValue", "java/lang/Integer") + "J" -> Pair("longValue", "java/lang/Long") + "S" -> Pair("shortValue", "java/lang/Short") + "Z" -> Pair("booleanValue", "java/lang/Boolean") + else -> return + } + mv.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + wrappedTypeDescriptor, + methodName, + "()$primitiveTypeDescriptor", + false, + ) + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt new file mode 100644 index 00000000..a26c0d6b --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt @@ -0,0 +1,132 @@ +// Copyright 2021 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.instrumentor + +import com.code_intelligence.jazzer.api.MethodHook +import com.code_intelligence.jazzer.api.MethodHooks +import com.code_intelligence.jazzer.utils.ClassNameGlobber +import com.code_intelligence.jazzer.utils.Log +import io.github.classgraph.ClassGraph +import io.github.classgraph.ScanResult +import java.lang.instrument.Instrumentation +import java.lang.reflect.Method +import java.util.jar.JarFile + +data class Hooks( + val hooks: List<Hook>, + val hookClasses: Set<Class<*>>, + val additionalHookClassNameGlobber: ClassNameGlobber, +) { + + companion object { + + fun appendHooksToBootstrapClassLoaderSearch(instrumentation: Instrumentation, hookClassNames: Set<String>) { + hookClassNames.mapNotNull { hook -> + val hookClassFilePath = "/${hook.replace('.', '/')}.class" + val hookClassFile = Companion::class.java.getResource(hookClassFilePath) ?: return@mapNotNull null + if ("jar" != hookClassFile.protocol) { + return@mapNotNull null + } + // hookClassFile.file looks as follows: + // file:/tmp/ExampleFuzzerHooks_deploy.jar!/com/example/ExampleFuzzerHooks.class + hookClassFile.file.removePrefix("file:").takeWhile { it != '!' } + } + .toSet() + .map { JarFile(it) } + .forEach { instrumentation.appendToBootstrapClassLoaderSearch(it) } + } + + fun loadHooks(excludeHookClassNames: List<String>, vararg hookClassNames: Set<String>): List<Hooks> { + return ClassGraph() + .enableClassInfo() + .enableSystemJarsAndModules() + .rejectPackages("jaz.*", "com.code_intelligence.jazzer.*") + .scan() + .use { scanResult -> + // Capture scanResult in HooksLoader field to not pass it through + // all internal hook loading methods. + val loader = HooksLoader(scanResult, excludeHookClassNames) + hookClassNames.map(loader::load) + } + } + + private class HooksLoader(private val scanResult: ScanResult, val excludeHookClassNames: List<String>) { + + fun load(hookClassNames: Set<String>): Hooks { + val hooksWithHookClasses = hookClassNames.flatMap(::loadHooks) + val hooks = hooksWithHookClasses.map { it.first } + val hookClasses = hooksWithHookClasses.map { it.second }.toSet() + val additionalHookClassNameGlobber = ClassNameGlobber( + hooks.flatMap(Hook::additionalClassesToHook), + excludeHookClassNames, + ) + return Hooks(hooks, hookClasses, additionalHookClassNameGlobber) + } + + private fun loadHooks(hookClassName: String): List<Pair<Hook, Class<*>>> { + return try { + // We let the static initializers of hook classes execute so that hooks can run + // code before the fuzz target class has been loaded (e.g., register themselves + // for the onFuzzTargetReady callback). + val hookClass = + Class.forName(hookClassName, true, Companion::class.java.classLoader) + loadHooks(hookClass).also { + Log.info("Loaded ${it.size} hooks from $hookClassName") + }.map { + it to hookClass + } + } catch (e: ClassNotFoundException) { + Log.warn("Failed to load hooks from $hookClassName", e) + emptyList() + } + } + + private fun loadHooks(hookClass: Class<*>): List<Hook> { + val hooks = mutableListOf<Hook>() + for (method in hookClass.methods.sortedBy { it.descriptor }) { + method.getAnnotation(MethodHook::class.java)?.let { + hooks.addAll(verifyAndGetHooks(method, it)) + } + method.getAnnotation(MethodHooks::class.java)?.let { + it.value.forEach { hookAnnotation -> + hooks.addAll(verifyAndGetHooks(method, hookAnnotation)) + } + } + } + return hooks + } + + private fun verifyAndGetHooks(hookMethod: Method, hookData: MethodHook): List<Hook> { + return lookupClassesToHook(hookData.targetClassName) + .map { className -> + Hook.createAndVerifyHook(hookMethod, hookData, className) + } + } + + private fun lookupClassesToHook(annotationTargetClassName: String): List<String> { + // Allowing arbitrary exterior whitespace in the target class name allows for an easy workaround + // for mangled hooks due to shading applied to hooks. + val targetClassName = annotationTargetClassName.trim() + val targetClassInfo = scanResult.getClassInfo(targetClassName) ?: return listOf(targetClassName) + val additionalTargetClasses = when { + targetClassInfo.isInterface -> scanResult.getClassesImplementing(targetClassName) + targetClassInfo.isAbstract -> scanResult.getSubclasses(targetClassName) + else -> emptyList() + } + return (listOf(targetClassName) + additionalTargetClasses.map { it.name }).sorted() + } + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt new file mode 100644 index 00000000..c6db94c0 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt @@ -0,0 +1,45 @@ +// Copyright 2021 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.instrumentor + +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.MethodNode + +enum class InstrumentationType { + CMP, + COV, + DIV, + GEP, + INDIR, + NATIVE, +} + +internal interface Instrumentor { + fun instrument(internalClassName: String, bytecode: ByteArray): ByteArray + + fun shouldInstrument(access: Int): Boolean { + return (access and Opcodes.ACC_ABSTRACT == 0) && + (access and Opcodes.ACC_NATIVE == 0) + } + + fun shouldInstrument(method: MethodNode): Boolean { + return shouldInstrument(method.access) && + method.instructions.size() > 0 + } + + companion object { + const val ASM_API_VERSION = Opcodes.ASM9 + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java b/src/main/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java new file mode 100644 index 00000000..0512ec2a --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java @@ -0,0 +1,48 @@ +// Copyright 2022 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.instrumentor; + +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.InstrSupport; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +public class StaticMethodStrategy implements EdgeCoverageStrategy { + @Override + public void instrumentControlFlowEdge( + MethodVisitor mv, int edgeId, int variable, String coverageMapInternalClassName) { + InstrSupport.push(mv, edgeId); + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, coverageMapInternalClassName, "recordCoverage", "(I)V", false); + } + + @Override + public int getInstrumentControlFlowEdgeStackSize() { + return 1; + } + + @Override + public Object getLocalVariableType() { + return null; + } + + @Override + public void loadLocalVariable( + MethodVisitor mv, int variable, String coverageMapInternalClassName) {} + + @Override + public int getLoadLocalVariableStackSize() { + return 0; + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt new file mode 100644 index 00000000..a46e72df --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt @@ -0,0 +1,268 @@ +// Copyright 2021 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.instrumentor + +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.InsnList +import org.objectweb.asm.tree.InsnNode +import org.objectweb.asm.tree.IntInsnNode +import org.objectweb.asm.tree.LdcInsnNode +import org.objectweb.asm.tree.LookupSwitchInsnNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.TableSwitchInsnNode + +internal class TraceDataFlowInstrumentor( + private val types: Set<InstrumentationType>, + private val callbackInternalClassName: String = "com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks", +) : Instrumentor { + + private lateinit var random: DeterministicRandom + + override fun instrument(internalClassName: String, bytecode: ByteArray): ByteArray { + val node = ClassNode() + val reader = ClassReader(bytecode) + reader.accept(node, 0) + random = DeterministicRandom("trace", node.name) + for (method in node.methods) { + if (shouldInstrument(method)) { + addDataFlowInstrumentation(method) + } + } + + val writer = ClassWriter(ClassWriter.COMPUTE_MAXS) + node.accept(writer) + return writer.toByteArray() + } + + private fun addDataFlowInstrumentation(method: MethodNode) { + loop@ for (inst in method.instructions.toArray()) { + when (inst.opcode) { + Opcodes.LCMP -> { + if (InstrumentationType.CMP !in types) continue@loop + method.instructions.insertBefore(inst, longCmpInstrumentation()) + method.instructions.remove(inst) + } + Opcodes.IF_ICMPEQ, Opcodes.IF_ICMPNE, + Opcodes.IF_ICMPLT, Opcodes.IF_ICMPLE, + Opcodes.IF_ICMPGT, Opcodes.IF_ICMPGE, + -> { + if (InstrumentationType.CMP !in types) continue@loop + method.instructions.insertBefore(inst, intCmpInstrumentation()) + } + Opcodes.IFEQ, Opcodes.IFNE, + Opcodes.IFLT, Opcodes.IFLE, + Opcodes.IFGT, Opcodes.IFGE, + -> { + if (InstrumentationType.CMP !in types) continue@loop + // The IF* opcodes are often used to branch based on the result of a compare + // instruction for a type other than int. The operands of this compare will + // already be reported via the instrumentation above (for non-floating point + // numbers) and the follow-up compare does not provide a good signal as all + // operands will be in {-1, 0, 1}. Skip instrumentation for it. + if (inst.previous?.opcode in listOf(Opcodes.DCMPG, Opcodes.DCMPL, Opcodes.FCMPG, Opcodes.DCMPL) || + (inst.previous as? MethodInsnNode)?.name == "traceCmpLongWrapper" + ) { + continue@loop + } + method.instructions.insertBefore(inst, ifInstrumentation()) + } + Opcodes.LOOKUPSWITCH, Opcodes.TABLESWITCH -> { + if (InstrumentationType.CMP !in types) continue@loop + // Mimic the exclusion logic for small label values in libFuzzer: + // https://github.com/llvm-mirror/compiler-rt/blob/69445f095c22aac2388f939bedebf224a6efcdaf/lib/fuzzer/FuzzerTracePC.cpp#L520 + // Case values are reported to libFuzzer via an array of unsigned long values and thus need to be + // sorted by unsigned value. + val caseValues = when (inst) { + is LookupSwitchInsnNode -> { + if (inst.keys.isEmpty() || (0 <= inst.keys.first() && inst.keys.last() < 256)) { + continue@loop + } + inst.keys + } + is TableSwitchInsnNode -> { + if (0 <= inst.min && inst.max < 256) { + continue@loop + } + (inst.min..inst.max).filter { caseValue -> + val index = caseValue - inst.min + // Filter out "gap cases". + inst.labels[index].label != inst.dflt.label + }.toList() + } + // Not reached. + else -> continue@loop + }.sortedBy { it.toUInt() }.map { it.toLong() }.toLongArray() + method.instructions.insertBefore(inst, switchInstrumentation(caseValues)) + } + Opcodes.IDIV -> { + if (InstrumentationType.DIV !in types) continue@loop + method.instructions.insertBefore(inst, intDivInstrumentation()) + } + Opcodes.LDIV -> { + if (InstrumentationType.DIV !in types) continue@loop + method.instructions.insertBefore(inst, longDivInstrumentation()) + } + Opcodes.AALOAD, Opcodes.BALOAD, + Opcodes.CALOAD, Opcodes.DALOAD, + Opcodes.FALOAD, Opcodes.IALOAD, + Opcodes.LALOAD, Opcodes.SALOAD, + -> { + if (InstrumentationType.GEP !in types) continue@loop + if (!isConstantIntegerPushInsn(inst.previous)) continue@loop + method.instructions.insertBefore(inst, gepLoadInstrumentation()) + } + Opcodes.INVOKEINTERFACE, Opcodes.INVOKESPECIAL, Opcodes.INVOKESTATIC, Opcodes.INVOKEVIRTUAL -> { + if (InstrumentationType.GEP !in types) continue@loop + if (!isGepLoadMethodInsn(inst as MethodInsnNode)) continue@loop + if (!isConstantIntegerPushInsn(inst.previous)) continue@loop + method.instructions.insertBefore(inst, gepLoadInstrumentation()) + } + } + } + } + + private fun InsnList.pushFakePc() { + add(LdcInsnNode(random.nextInt(512))) + } + + private fun longCmpInstrumentation() = InsnList().apply { + pushFakePc() + // traceCmpLong returns the result of the comparison as duplicating two longs on the stack + // is not possible without local variables. + add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceCmpLongWrapper", "(JJI)I", false)) + } + + private fun intCmpInstrumentation() = InsnList().apply { + add(InsnNode(Opcodes.DUP2)) + pushFakePc() + add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceCmpInt", "(III)V", false)) + } + + private fun ifInstrumentation() = InsnList().apply { + add(InsnNode(Opcodes.DUP)) + // All if* instructions are compares to the constant 0. + add(InsnNode(Opcodes.ICONST_0)) + add(InsnNode(Opcodes.SWAP)) + pushFakePc() + add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceConstCmpInt", "(III)V", false)) + } + + private fun intDivInstrumentation() = InsnList().apply { + add(InsnNode(Opcodes.DUP)) + pushFakePc() + add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceDivInt", "(II)V", false)) + } + + private fun longDivInstrumentation() = InsnList().apply { + add(InsnNode(Opcodes.DUP2)) + pushFakePc() + add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceDivLong", "(JI)V", false)) + } + + private fun switchInstrumentation(caseValues: LongArray) = InsnList().apply { + // duplicate {lookup,table}switch key for use as first function argument + add(InsnNode(Opcodes.DUP)) + add(InsnNode(Opcodes.I2L)) + // Set up array with switch case values. The format libfuzzer expects is created here directly, i.e., the first + // two entries are the number of cases and the bit size of values (always 32). + add(IntInsnNode(Opcodes.SIPUSH, caseValues.size + 2)) + add(IntInsnNode(Opcodes.NEWARRAY, Opcodes.T_LONG)) + // Store number of cases + add(InsnNode(Opcodes.DUP)) + add(IntInsnNode(Opcodes.SIPUSH, 0)) + add(LdcInsnNode(caseValues.size.toLong())) + add(InsnNode(Opcodes.LASTORE)) + // Store bit size of keys + add(InsnNode(Opcodes.DUP)) + add(IntInsnNode(Opcodes.SIPUSH, 1)) + add(LdcInsnNode(32.toLong())) + add(InsnNode(Opcodes.LASTORE)) + // Store {lookup,table}switch case values + for ((i, caseValue) in caseValues.withIndex()) { + add(InsnNode(Opcodes.DUP)) + add(IntInsnNode(Opcodes.SIPUSH, 2 + i)) + add(LdcInsnNode(caseValue)) + add(InsnNode(Opcodes.LASTORE)) + } + pushFakePc() + // call the native callback function + add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceSwitch", "(J[JI)V", false)) + } + + /** + * Returns true if [node] represents an instruction that possibly pushes a valid, non-zero, constant array index + * onto the stack. + */ + private fun isConstantIntegerPushInsn(node: AbstractInsnNode?) = node?.opcode in CONSTANT_INTEGER_PUSH_OPCODES + + /** + * Returns true if [node] represents a call to a method that performs an indexed lookup into an array-like + * structure. + */ + private fun isGepLoadMethodInsn(node: MethodInsnNode): Boolean { + if (!node.desc.startsWith("(I)")) return false + val returnType = node.desc.removePrefix("(I)") + return MethodInfo(node.owner, node.name, returnType) in GEP_LOAD_METHODS + } + + private fun gepLoadInstrumentation() = InsnList().apply { + // Duplicate the index and convert to long. + add(InsnNode(Opcodes.DUP)) + add(InsnNode(Opcodes.I2L)) + pushFakePc() + add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceGep", "(JI)V", false)) + } + + companion object { + // Low constants (0, 1) are omitted as they create a lot of noise. + val CONSTANT_INTEGER_PUSH_OPCODES = listOf( + Opcodes.BIPUSH, + Opcodes.SIPUSH, + Opcodes.LDC, + Opcodes.ICONST_2, + Opcodes.ICONST_3, + Opcodes.ICONST_4, + Opcodes.ICONST_5, + ) + + data class MethodInfo(val internalClassName: String, val name: String, val returnType: String) + + val GEP_LOAD_METHODS = setOf( + MethodInfo("java/util/AbstractList", "get", "Ljava/lang/Object;"), + MethodInfo("java/util/ArrayList", "get", "Ljava/lang/Object;"), + MethodInfo("java/util/List", "get", "Ljava/lang/Object;"), + MethodInfo("java/util/Stack", "get", "Ljava/lang/Object;"), + MethodInfo("java/util/Vector", "get", "Ljava/lang/Object;"), + MethodInfo("java/lang/CharSequence", "charAt", "C"), + MethodInfo("java/lang/String", "charAt", "C"), + MethodInfo("java/lang/StringBuffer", "charAt", "C"), + MethodInfo("java/lang/StringBuilder", "charAt", "C"), + MethodInfo("java/lang/String", "codePointAt", "I"), + MethodInfo("java/lang/String", "codePointBefore", "I"), + MethodInfo("java/nio/ByteBuffer", "get", "B"), + MethodInfo("java/nio/ByteBuffer", "getChar", "C"), + MethodInfo("java/nio/ByteBuffer", "getDouble", "D"), + MethodInfo("java/nio/ByteBuffer", "getFloat", "F"), + MethodInfo("java/nio/ByteBuffer", "getInt", "I"), + MethodInfo("java/nio/ByteBuffer", "getLong", "J"), + MethodInfo("java/nio/ByteBuffer", "getShort", "S"), + ) + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/jazzer_shade_rules.jarjar b/src/main/java/com/code_intelligence/jazzer/jazzer_shade_rules.jarjar new file mode 100644 index 00000000..91073722 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/jazzer_shade_rules.jarjar @@ -0,0 +1,6 @@ +rule com.github.** com.code_intelligence.jazzer.third_party.@0 +rule io.** com.code_intelligence.jazzer.third_party.@0 +rule kotlin.** com.code_intelligence.jazzer.third_party.@0 +rule net.** com.code_intelligence.jazzer.third_party.@0 +rule nonapi.** com.code_intelligence.jazzer.third_party.@0 +rule org.objectweb.** com.code_intelligence.jazzer.third_party.@0 diff --git a/src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java b/src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java new file mode 100644 index 00000000..1f286a31 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java @@ -0,0 +1,72 @@ +// Copyright 2022 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.junit; + +import static com.code_intelligence.jazzer.junit.Utils.getClassPathBasedInstrumentationFilter; +import static com.code_intelligence.jazzer.junit.Utils.getLegacyInstrumentationFilter; + +import java.io.File; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.extension.ExtensionContext; + +class AgentConfigurator { + private static final AtomicBoolean hasBeenConfigured = new AtomicBoolean(); + + static void forRegressionTest(ExtensionContext extensionContext) { + if (!hasBeenConfigured.compareAndSet(false, true)) { + return; + } + + applyCommonConfiguration(); + + // Add logic to the hook instrumentation that allows us to enable and disable hooks at runtime. + System.setProperty("jazzer.internal.conditional_hooks", "true"); + // Apply all hooks, but no coverage or compare instrumentation. + System.setProperty("jazzer.instrumentation_excludes", "**"); + extensionContext.getConfigurationParameter("jazzer.instrument") + .ifPresent(s + -> System.setProperty( + "jazzer.custom_hook_includes", String.join(File.pathSeparator, s.split(",")))); + } + + static void forFuzzing(ExtensionContext executionRequest) { + if (!hasBeenConfigured.compareAndSet(false, true)) { + throw new IllegalStateException("Only a single fuzz test should be executed per fuzzing run"); + } + + applyCommonConfiguration(); + + String instrumentationFilter = + executionRequest.getConfigurationParameter("jazzer.instrument") + .orElseGet( + () + -> getClassPathBasedInstrumentationFilter(System.getProperty("java.class.path")) + .orElseGet(() + -> getLegacyInstrumentationFilter( + executionRequest.getRequiredTestClass()))); + String filter = String.join(File.pathSeparator, instrumentationFilter.split(",")); + System.setProperty("jazzer.custom_hook_includes", filter); + System.setProperty("jazzer.instrumentation_includes", filter); + } + + private static void applyCommonConfiguration() { + // Do not hook common IDE and JUnit classes and their dependencies. + System.setProperty("jazzer.custom_hook_excludes", + String.join(File.pathSeparator, "com.google.testing.junit.**", "com.intellij.**", + "org.jetbrains.**", "io.github.classgraph.**", "junit.framework.**", "net.bytebuddy.**", + "org.apiguardian.**", "org.assertj.core.**", "org.hamcrest.**", "org.junit.**", + "org.opentest4j.**", "org.mockito.**", "org.apache.maven.**", "org.gradle.**")); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/junit/AgentConfiguringArgumentsProvider.java b/src/main/java/com/code_intelligence/jazzer/junit/AgentConfiguringArgumentsProvider.java new file mode 100644 index 00000000..e65f028b --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/junit/AgentConfiguringArgumentsProvider.java @@ -0,0 +1,43 @@ +/* + * 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.junit; + +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.support.AnnotationConsumer; + +public class AgentConfiguringArgumentsProvider + implements ArgumentsProvider, AnnotationConsumer<FuzzTest> { + private FuzzTest fuzzTest; + + @Override + public void accept(FuzzTest fuzzTest) { + this.fuzzTest = fuzzTest; + } + + @Override + public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) + throws Exception { + // FIXME(fmeum): Calling this here feels like a hack. There should be a lifecycle hook that runs + // before the argument discovery for a ParameterizedTest is kicked off, but I haven't found + // one. + FuzzTestExecutor.configureAndInstallAgent(extensionContext, fuzzTest.maxDuration()); + return Stream.empty(); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel new file mode 100644 index 00000000..3eb8959f --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel @@ -0,0 +1,99 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library") + +java_library( + name = "junit", + visibility = ["//deploy:__pkg__"], + runtime_deps = [ + ":fuzz_test", + ], +) + +java_library( + name = "agent_configurator", + srcs = [ + "AgentConfigurator.java", + ], + deps = [ + ":utils", + "@maven//:org_junit_jupiter_junit_jupiter_api", + ], +) + +java_library( + name = "fuzz_test", + srcs = [ + "AgentConfiguringArgumentsProvider.java", + "FuzzTest.java", + "FuzzTestExtensions.java", + "FuzzingArgumentsProvider.java", + "SeedArgumentsProvider.java", + ], + visibility = [ + "//examples/junit/src/test/java/com/example:__pkg__", + ], + runtime_deps = [ + # The JUnit launcher that is part of the Jazzer driver needs this on the classpath + # to run an @FuzzTest with JUnit. This will also result in a transitive dependency + # in the generated pom file. + "@maven//:org_junit_platform_junit_platform_launcher", + ], + deps = [ + ":fuzz_test_executor", + ":seed_serializer", + ":utils", + "@maven//:org_junit_jupiter_junit_jupiter_api", + "@maven//:org_junit_jupiter_junit_jupiter_params", + "@maven//:org_junit_platform_junit_platform_commons", + ], +) + +java_jni_library( + name = "fuzz_test_executor", + srcs = [ + "FuzzTestExecutor.java", + ], + native_libs = [ + "//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver", + ], + deps = [ + ":agent_configurator", + ":seed_serializer", + ":utils", + "//src/main/java/com/code_intelligence/jazzer/agent:agent_installer", + "//src/main/java/com/code_intelligence/jazzer/api", + "//src/main/java/com/code_intelligence/jazzer/autofuzz", + "//src/main/java/com/code_intelligence/jazzer/driver:fuzz_target_holder", + "//src/main/java/com/code_intelligence/jazzer/driver:fuzz_target_runner", + "//src/main/java/com/code_intelligence/jazzer/driver:opt", + "//src/main/java/com/code_intelligence/jazzer/driver/junit:exit_code_exception", + "//src/main/java/com/code_intelligence/jazzer/mutation", + "//src/main/java/com/code_intelligence/jazzer/utils", + "@maven//:org_junit_jupiter_junit_jupiter_api", + "@maven//:org_junit_jupiter_junit_jupiter_params", + "@maven//:org_junit_platform_junit_platform_commons", + ], +) + +java_library( + name = "seed_serializer", + srcs = ["SeedSerializer.java"], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/api", + "//src/main/java/com/code_intelligence/jazzer/autofuzz", + "//src/main/java/com/code_intelligence/jazzer/driver:fuzzed_data_provider_impl", + "//src/main/java/com/code_intelligence/jazzer/driver:opt", + "//src/main/java/com/code_intelligence/jazzer/mutation", + ], +) + +java_library( + name = "utils", + srcs = ["Utils.java"], + visibility = ["//src/test/java/com/code_intelligence/jazzer/junit:__pkg__"], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider", + "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_utils", + "@maven//:org_junit_jupiter_junit_jupiter_api", + "@maven//:org_junit_jupiter_junit_jupiter_params", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTest.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTest.java new file mode 100644 index 00000000..041db96d --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTest.java @@ -0,0 +1,122 @@ +// Copyright 2022 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.junit; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.api.parallel.Resources; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; + +/** + * A parameterized test with parameters generated automatically by the Java fuzzer <a + * href="https://github.com/CodeIntelligenceTesting/jazzer">Jazzer</a>. + * + * <h2>Test parameters</h2> + * + * <p>Methods annotated with {@link FuzzTest} can take either of the following types of parameters: + * + * <dl> + * <dt>{@code byte[]}</dt> + * <dd>Raw byte input mutated by the fuzzer. Use this signature when your fuzz test naturally + * handles raw bytes (e.g. when fuzzing a binary format parser). This is the most efficient, but + * also the least convenient way to write a fuzz test.</dd> + * + * <dt>{@link com.code_intelligence.jazzer.api.FuzzedDataProvider}</dt> + * <dd>Provides convenience methods that generate instances of commonly used Java types from the raw + * fuzzer input. This is generally the best way to write fuzz tests.</dd> + * + * <dt>any non-zero number of parameters of any type</dt> + * <dd>In this case, Jazzer will rely on reflection and class path scanning to instantiate concrete + * arguments. While convenient and a good way to get started, fuzz tests using this feature will + * generally be less efficient than fuzz tests using any of the other possible signatures. Due to + * the reliance on class path scanning, any change to the class path may also render previous + * findings unreproducible.</dd> + * </dl> + * + * <h2>Test modes</h2> + * + * A fuzz test can be run in two modes: fuzzing and regression testing. + * + * <h3>Fuzzing</h3> + * <p>When the environment variable {@code JAZZER_FUZZ} is set to any non-empty value, fuzz tests + * run in "fuzzing" mode. In this mode, the method annotated with {@link FuzzTest} are invoked + * repeatedly with inputs that Jazzer generates and mutates based on feedback obtained from + * instrumentation it applies to the test and every class loaded by it. + * + * <p>When an assertion in the test fails, an exception is thrown but not caught, or Jazzer's + * instrumentation detects a security issue (e.g. SQL injection or insecure deserialization), the + * fuzz test is reported as failed and the input is collected in the inputs directory for the test + * class (see "Regression testing" for details). + * + * <p>When no issue has been found after the configured {@link FuzzTest#maxDuration()}, the test + * passes. + * + * <p>Only a single fuzz test per test run will be executed in fuzzing mode. All other fuzz tests + * will be skipped. + * + * <h3>Regression testing</h3> + * <p>By default, a fuzz test is executed as a regular JUnit {@link ParameterizedTest} running on a + * fixed set of inputs. It can be run together with regular unit tests and used to verify that past + * findings remain fixed. In IDEs with JUnit 5 integration, it can also be used to conveniently + * debug individual findings. + * + * <p>Fuzz tests are always executed on the empty input as well as all input files contained in the + * resource directory called {@code <TestClassName>Inputs} in the current package. For example, + * all fuzz tests contained in the class {@code com.example.MyFuzzTests} would run on all files + * under {@code src/test/resources/com/example/MyFuzzTestsInputs}. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@AgentConfiguringArgumentsProviderArgumentsSource +@ArgumentsSource(SeedArgumentsProvider.class) +@FuzzingArgumentsProviderArgumentsSource +@ExtendWith(FuzzTestExtensions.class) +// {0} is expanded to the basename of the seed by the ArgumentProvider. +@ParameterizedTest(name = "{0}") +@Tag("jazzer") +// Fuzz tests can't run in parallel with other fuzz tests since the last finding is kept in a global +// variable. +// Fuzz tests also can't run in parallel with other non-fuzz tests since method hooks are enabled +// conditionally based on a global variable. +@ResourceLock(value = Resources.GLOBAL, mode = ResourceAccessMode.READ_WRITE) +public @interface FuzzTest { + /** + * A duration string such as "1h 2m 30s" indicating for how long the fuzz test should be executed + * during fuzzing. + * + * <p>This option has no effect during regression testing. + */ + String maxDuration() default "5m"; +} + +// Internal use only. +// These wrappers are needed only because the container annotation for @ArgumentsSource, +// @ArgumentsSources, can't be applied to annotations. +@Target({ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ArgumentsSource(AgentConfiguringArgumentsProvider.class) +@interface AgentConfiguringArgumentsProviderArgumentsSource {} + +@Target({ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ArgumentsSource(FuzzingArgumentsProvider.class) +@interface FuzzingArgumentsProviderArgumentsSource {} diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java new file mode 100644 index 00000000..49252e84 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java @@ -0,0 +1,282 @@ +// 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.junit; + +import static com.code_intelligence.jazzer.junit.Utils.durationStringToSeconds; +import static com.code_intelligence.jazzer.junit.Utils.generatedCorpusPath; +import static com.code_intelligence.jazzer.junit.Utils.inputsDirectoryResourcePath; +import static com.code_intelligence.jazzer.junit.Utils.inputsDirectorySourcePath; + +import com.code_intelligence.jazzer.agent.AgentInstaller; +import com.code_intelligence.jazzer.driver.FuzzTargetHolder; +import com.code_intelligence.jazzer.driver.FuzzTargetRunner; +import com.code_intelligence.jazzer.driver.Opt; +import com.code_intelligence.jazzer.driver.junit.ExitCodeException; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.platform.commons.support.AnnotationSupport; + +class FuzzTestExecutor { + private static final AtomicBoolean hasBeenPrepared = new AtomicBoolean(); + private static final AtomicBoolean agentInstalled = new AtomicBoolean(false); + + private final List<String> libFuzzerArgs; + private final Path javaSeedsDir; + private final boolean isRunFromCommandLine; + + private FuzzTestExecutor( + List<String> libFuzzerArgs, Path javaSeedsDir, boolean isRunFromCommandLine) { + this.libFuzzerArgs = libFuzzerArgs; + this.javaSeedsDir = javaSeedsDir; + this.isRunFromCommandLine = isRunFromCommandLine; + } + + public static FuzzTestExecutor prepare(ExtensionContext context, String maxDuration) + throws IOException { + if (!hasBeenPrepared.compareAndSet(false, true)) { + throw new IllegalStateException( + "FuzzTestExecutor#prepare can only be called once per test run"); + } + + Class<?> fuzzTestClass = context.getRequiredTestClass(); + Method fuzzTestMethod = context.getRequiredTestMethod(); + + List<ArgumentsSource> allSources = AnnotationSupport.findRepeatableAnnotations( + context.getRequiredTestMethod(), ArgumentsSource.class); + // Non-empty as it always contains FuzzingArgumentsProvider. + ArgumentsSource lastSource = allSources.get(allSources.size() - 1); + // Ensure that our ArgumentsProviders run last so that we can record all the seeds generated by + // user-provided ones. + if (lastSource.value().getPackage() != FuzzTestExecutor.class.getPackage()) { + throw new IllegalArgumentException("@FuzzTest must be the last annotation on a fuzz test," + + " but it came after the (meta-)annotation " + lastSource); + } + + Path baseDir = + Paths.get(context.getConfigurationParameter("jazzer.internal.basedir").orElse("")) + .toAbsolutePath(); + + List<String> originalLibFuzzerArgs = getLibFuzzerArgs(context); + String argv0 = originalLibFuzzerArgs.isEmpty() ? "fake_argv0" : originalLibFuzzerArgs.remove(0); + + ArrayList<String> libFuzzerArgs = new ArrayList<>(); + libFuzzerArgs.add(argv0); + + // Add passed in corpus directories (and files) at the beginning of the arguments list. + // libFuzzer uses the first directory to store discovered inputs, whereas all others are + // only used to provide additional seeds and aren't written into. + List<String> corpusDirs = originalLibFuzzerArgs.stream() + .filter(arg -> !arg.startsWith("-")) + .collect(Collectors.toList()); + originalLibFuzzerArgs.removeAll(corpusDirs); + libFuzzerArgs.addAll(corpusDirs); + + // Use the specified corpus dir, if given, otherwise store the generated corpus in a per-class + // directory under the project root, just like cifuzz: + // https://github.com/CodeIntelligenceTesting/cifuzz/blob/bf410dcfbafbae2a73cf6c5fbed031cdfe234f2f/internal/cmd/run/run.go#L381 + // The path is specified relative to the current working directory, which with JUnit is the + // project directory. + Path generatedCorpusDir = baseDir.resolve(generatedCorpusPath(fuzzTestClass, fuzzTestMethod)); + Files.createDirectories(generatedCorpusDir); + libFuzzerArgs.add(generatedCorpusDir.toAbsolutePath().toString()); + + // We can only emit findings into the source tree version of the inputs directory, not e.g. the + // copy under Maven's target directory. If it doesn't exist, collect the inputs in the current + // working directory, which is usually the project's source root. + Optional<Path> findingsDirectory = + inputsDirectorySourcePath(fuzzTestClass, fuzzTestMethod, baseDir); + if (!findingsDirectory.isPresent()) { + context.publishReportEntry(String.format( + "Collecting crashing inputs in the project root directory.\nIf you want to keep them " + + "organized by fuzz test and automatically run them as regression tests with " + + "JUnit Jupiter, create a test resource directory called '%s' in package '%s' " + + "and move the files there.", + inputsDirectoryResourcePath(fuzzTestClass, fuzzTestMethod), + fuzzTestClass.getPackage().getName())); + } + + // We prefer the inputs directory on the classpath, if it exists, as that is more reliable than + // heuristically looking into the source tree based on the current working directory. + Optional<Path> inputsDirectory; + URL inputsDirectoryUrl = + fuzzTestClass.getResource(inputsDirectoryResourcePath(fuzzTestClass, fuzzTestMethod)); + if (inputsDirectoryUrl != null && "file".equals(inputsDirectoryUrl.getProtocol())) { + // The inputs directory is a regular directory on disk (i.e., the test is not run from a + // JAR). + try { + // Using inputsDirectoryUrl.getFile() fails on Windows. + inputsDirectory = Optional.of(Paths.get(inputsDirectoryUrl.toURI())); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } else { + if (inputsDirectoryUrl != null && !findingsDirectory.isPresent()) { + context.publishReportEntry( + "When running Jazzer fuzz tests from a JAR rather than class files, the inputs " + + "directory isn't used unless it is located under src/test/resources/..."); + } + inputsDirectory = findingsDirectory; + } + + // From the second positional argument on, files and directories are used as seeds but not + // modified. + inputsDirectory.ifPresent(dir -> libFuzzerArgs.add(dir.toAbsolutePath().toString())); + Path javaSeedsDir = Files.createTempDirectory("jazzer-java-seeds"); + libFuzzerArgs.add(javaSeedsDir.toAbsolutePath().toString()); + libFuzzerArgs.add(String.format("-artifact_prefix=%s%c", + findingsDirectory.orElse(baseDir).toAbsolutePath(), File.separatorChar)); + + libFuzzerArgs.add("-max_total_time=" + durationStringToSeconds(maxDuration)); + // Disable libFuzzer's out of memory detection: It is only useful for native library fuzzing, + // which we don't support without our native driver, and leads to false positives where it picks + // up IntelliJ's memory usage. + libFuzzerArgs.add("-rss_limit_mb=0"); + if (Utils.permissivelyParseBoolean( + context.getConfigurationParameter("jazzer.valueprofile").orElse("false"))) { + libFuzzerArgs.add("-use_value_profile=1"); + } + + // Prefer original libFuzzerArgs set via command line by appending them last. + libFuzzerArgs.addAll(originalLibFuzzerArgs); + + return new FuzzTestExecutor(libFuzzerArgs, javaSeedsDir, Utils.runFromCommandLine(context)); + } + + /** + * Returns the list of arguments set on the command line. + */ + private static List<String> getLibFuzzerArgs(ExtensionContext extensionContext) { + List<String> args = new ArrayList<>(); + for (int i = 0;; i++) { + Optional<String> arg = extensionContext.getConfigurationParameter("jazzer.internal.arg." + i); + if (!arg.isPresent()) { + break; + } + args.add(arg.get()); + } + return args; + } + + static void configureAndInstallAgent(ExtensionContext extensionContext, String maxDuration) + throws IOException { + if (!agentInstalled.compareAndSet(false, true)) { + return; + } + if (Utils.isFuzzing(extensionContext)) { + FuzzTestExecutor executor = prepare(extensionContext, maxDuration); + extensionContext.getRoot().getStore(Namespace.GLOBAL).put(FuzzTestExecutor.class, executor); + AgentConfigurator.forFuzzing(extensionContext); + } else { + AgentConfigurator.forRegressionTest(extensionContext); + } + AgentInstaller.install(Opt.hooks); + } + + static FuzzTestExecutor fromContext(ExtensionContext extensionContext) { + return extensionContext.getRoot() + .getStore(Namespace.GLOBAL) + .get(FuzzTestExecutor.class, FuzzTestExecutor.class); + } + + public void addSeed(byte[] bytes) throws IOException { + Path seed = Files.createTempFile(javaSeedsDir, "seed", null); + Files.write(seed, bytes); + } + + @SuppressWarnings("OptionalGetWithoutIsPresent") + public Optional<Throwable> execute( + ReflectiveInvocationContext<Method> invocationContext, SeedSerializer seedSerializer) { + if (seedSerializer instanceof AutofuzzSeedSerializer) { + FuzzTargetHolder.fuzzTarget = FuzzTargetHolder.autofuzzFuzzTarget(() -> { + // Provide an empty throws declaration to prevent autofuzz from + // ignoring the defined test exceptions. All exceptions in tests + // should cause them to fail. + Map<Executable, Class<?>[]> throwsDeclarations = new HashMap<>(1); + throwsDeclarations.put(invocationContext.getExecutable(), new Class[0]); + + com.code_intelligence.jazzer.autofuzz.FuzzTarget.setTarget( + new Executable[] {invocationContext.getExecutable()}, + invocationContext.getTarget().get(), invocationContext.getExecutable().toString(), + Collections.emptySet(), throwsDeclarations); + return null; + }); + } else { + FuzzTargetHolder.fuzzTarget = + new FuzzTargetHolder.FuzzTarget(invocationContext.getExecutable(), + () -> invocationContext.getTarget().get(), Optional.empty()); + } + + // Only register a finding handler in case the fuzz test is executed by JUnit. + // It short-circuits the handling in FuzzTargetRunner and prevents settings + // like --keep_going. + AtomicReference<Throwable> atomicFinding = new AtomicReference<>(); + if (!isRunFromCommandLine) { + FuzzTargetRunner.registerFindingHandler(t -> { + atomicFinding.set(t); + return false; + }); + } + + int exitCode = FuzzTargetRunner.startLibFuzzer(libFuzzerArgs); + deleteJavaSeedsDir(); + Throwable finding = atomicFinding.get(); + if (finding != null) { + return Optional.of(finding); + } else if (exitCode != 0) { + return Optional.of( + new ExitCodeException("Jazzer exited with exit code " + exitCode, exitCode)); + } else { + return Optional.empty(); + } + } + + private void deleteJavaSeedsDir() { + // The directory only consists of files, which we need to delete before deleting the directory + // itself. + try (Stream<Path> entries = Files.list(javaSeedsDir)) { + entries.forEach(FuzzTestExecutor::deleteIgnoringErrors); + } catch (IOException ignored) { + } + deleteIgnoringErrors(javaSeedsDir); + } + + private static void deleteIgnoringErrors(Path path) { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExtensions.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExtensions.java new file mode 100644 index 00000000..0e8ceec9 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExtensions.java @@ -0,0 +1,170 @@ +// Copyright 2022 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.junit; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.InvocationInterceptor; +import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import org.junit.platform.commons.support.AnnotationSupport; + +class FuzzTestExtensions implements ExecutionCondition, InvocationInterceptor { + private static final String JAZZER_INTERNAL = + "com.code_intelligence.jazzer.runtime.JazzerInternal"; + private static final AtomicReference<Method> fuzzTestMethod = new AtomicReference<>(); + private static Field lastFindingField; + private static Field hooksEnabledField; + + @Override + public void interceptTestTemplateMethod(Invocation<Void> invocation, + ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) + throws Throwable { + FuzzTest fuzzTest = + AnnotationSupport.findAnnotation(invocationContext.getExecutable(), FuzzTest.class).get(); + FuzzTestExecutor.configureAndInstallAgent(extensionContext, fuzzTest.maxDuration()); + // Skip the invocation of the test method with the special arguments provided by + // FuzzTestArgumentsProvider and start fuzzing instead. + if (Utils.isMarkedInvocation(invocationContext)) { + startFuzzing(invocation, invocationContext, extensionContext); + } else { + // Blocked by https://github.com/junit-team/junit5/issues/3282: + // TODO: The seeds from the input directory are duplicated here as there is no way to + // recognize them. + // TODO: Error out if there is a non-Jazzer ArgumentsProvider and the SeedSerializer does not + // support write. + if (Utils.isFuzzing(extensionContext)) { + // JUnit verifies that the arguments for this invocation are valid. + recordSeedForFuzzing(invocationContext.getArguments(), extensionContext); + } + runWithHooks(invocation); + } + } + + /** + * Mimics the logic of Jazzer's FuzzTargetRunner, which reports findings in the following way: + * <ol> + * <li>If a hook used Jazzer#reportFindingFromHook to explicitly report a finding, the last such + * finding, as stored in JazzerInternal#lastFinding, is reported. <li>If the fuzz target method + * threw a Throwable, that is reported. <li>3. Otherwise, nothing is reported. + * </ol> + */ + private static void runWithHooks(Invocation<Void> invocation) throws Throwable { + Throwable thrown = null; + getLastFindingField().set(null, null); + // When running in regression test mode, the agent emits additional bytecode logic in front of + // method hook invocations that enables them only while a global variable managed by + // withHooksEnabled is true. + // + // Alternatives considered: + // * Using a dedicated class loader for @FuzzTests: First-class support for this isn't + // available in JUnit 5 (https://github.com/junit-team/junit5/issues/201), but + // third-party extensions have done it: + // https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtension.java + // However, as this involves launching a new test run as part of running a test, this + // introduces a number of inconsistencies if applied on the test method rather than test + // class level. For example, @BeforeAll methods will have to be run twice in different class + // loaders, which may not be safe if they are using global resources not separated by class + // loaders (e.g. files). + try (AutoCloseable ignored = withHooksEnabled()) { + invocation.proceed(); + } catch (Throwable t) { + thrown = t; + } + Throwable stored = (Throwable) getLastFindingField().get(null); + if (stored != null) { + throw stored; + } else if (thrown != null) { + throw thrown; + } + } + + private static void startFuzzing(Invocation<Void> invocation, + ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) + throws Throwable { + invocation.skip(); + Optional<Throwable> throwable = + FuzzTestExecutor.fromContext(extensionContext) + .execute(invocationContext, getOrCreateSeedSerializer(extensionContext)); + if (throwable.isPresent()) { + throw throwable.get(); + } + } + + private void recordSeedForFuzzing(List<Object> arguments, ExtensionContext extensionContext) + throws IOException { + SeedSerializer seedSerializer = getOrCreateSeedSerializer(extensionContext); + try { + FuzzTestExecutor.fromContext(extensionContext) + .addSeed(seedSerializer.write(arguments.toArray())); + } catch (UnsupportedOperationException ignored) { + } + } + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext extensionContext) { + if (!Utils.isFuzzing(extensionContext)) { + return ConditionEvaluationResult.enabled( + "Regression tests are run instead of fuzzing since JAZZER_FUZZ has not been set to a non-empty value"); + } + // Only fuzz the first @FuzzTest that makes it here. + if (FuzzTestExtensions.fuzzTestMethod.compareAndSet( + null, extensionContext.getRequiredTestMethod()) + || extensionContext.getRequiredTestMethod().equals( + FuzzTestExtensions.fuzzTestMethod.get())) { + return ConditionEvaluationResult.enabled( + "Fuzzing " + extensionContext.getRequiredTestMethod()); + } + return ConditionEvaluationResult.disabled( + "Only one fuzz test can be run at a time, but multiple tests have been annotated with @FuzzTest"); + } + + private static SeedSerializer getOrCreateSeedSerializer(ExtensionContext extensionContext) { + Method method = extensionContext.getRequiredTestMethod(); + return extensionContext.getStore(Namespace.create(FuzzTestExtensions.class, method)) + .getOrComputeIfAbsent( + SeedSerializer.class, unused -> SeedSerializer.of(method), SeedSerializer.class); + } + + private static Field getLastFindingField() throws ClassNotFoundException, NoSuchFieldException { + if (lastFindingField == null) { + Class<?> jazzerInternal = Class.forName(JAZZER_INTERNAL); + lastFindingField = jazzerInternal.getField("lastFinding"); + } + return lastFindingField; + } + + private static Field getHooksEnabledField() throws ClassNotFoundException, NoSuchFieldException { + if (hooksEnabledField == null) { + Class<?> jazzerInternal = Class.forName(JAZZER_INTERNAL); + hooksEnabledField = jazzerInternal.getField("hooksEnabled"); + } + return hooksEnabledField; + } + + private static AutoCloseable withHooksEnabled() + throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException { + Field hooksEnabledField = getHooksEnabledField(); + hooksEnabledField.setBoolean(null, true); + return () -> hooksEnabledField.setBoolean(null, false); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzingArgumentsProvider.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzingArgumentsProvider.java new file mode 100644 index 00000000..61a8d3fe --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzingArgumentsProvider.java @@ -0,0 +1,42 @@ +/* + * 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.junit; + +import static com.code_intelligence.jazzer.junit.Utils.isFuzzing; + +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +class FuzzingArgumentsProvider implements ArgumentsProvider { + @Override + public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) { + if (!isFuzzing(extensionContext)) { + return Stream.empty(); + } + + // When fuzzing, supply a special set of arguments that our InvocationInterceptor uses as a + // sign to start fuzzing. + // FIXME: This is a hack that is needed only because there does not seem to be a way to + // communicate out of band that a certain invocation was triggered by a particular argument + // provider. We should get rid of this hack as soon as + // https://github.com/junit-team/junit5/issues/3282 has been addressed. + return Stream.of( + Utils.getMarkedArguments(extensionContext.getRequiredTestMethod(), "Fuzzing...")); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/junit/SeedArgumentsProvider.java b/src/main/java/com/code_intelligence/jazzer/junit/SeedArgumentsProvider.java new file mode 100644 index 00000000..687a1f7f --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/junit/SeedArgumentsProvider.java @@ -0,0 +1,225 @@ +// Copyright 2022 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.junit; + +import static com.code_intelligence.jazzer.junit.Utils.isFuzzing; +import static com.code_intelligence.jazzer.junit.Utils.runFromCommandLine; +import static org.junit.jupiter.api.Named.named; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiPredicate; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +class SeedArgumentsProvider implements ArgumentsProvider { + @Override + public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) + throws IOException { + if (runFromCommandLine(extensionContext)) { + // libFuzzer always runs on the file-based seeds first anyway and the additional visual + // indication provided by test invocations for seeds isn't effective on the command line, so + // we skip these invocations. + return Stream.empty(); + } + + Class<?> testClass = extensionContext.getRequiredTestClass(); + Method testMethod = extensionContext.getRequiredTestMethod(); + + Stream<Map.Entry<String, byte[]>> rawSeeds = + Stream.of(new SimpleImmutableEntry<>("<empty input>", new byte[0])); + rawSeeds = Stream.concat(rawSeeds, walkInputs(testClass, testMethod)); + + if (Utils.isCoverageAgentPresent() + && Files.isDirectory(Utils.generatedCorpusPath(testClass, testMethod))) { + rawSeeds = Stream.concat(rawSeeds, + walkInputsInPath(Utils.generatedCorpusPath(testClass, testMethod), Integer.MAX_VALUE)); + } + + SeedSerializer serializer = SeedSerializer.of(testMethod); + return rawSeeds + .map(entry -> { + Object[] args = serializer.read(entry.getValue()); + args[0] = named(entry.getKey(), args[0]); + return arguments(args); + }) + .onClose(() -> { + if (!isFuzzing(extensionContext)) { + extensionContext.publishReportEntry( + "No fuzzing has been performed, the fuzz test has only been executed on the fixed " + + "set of inputs in the seed corpus.\n" + + "To start fuzzing, run a test with the environment variable JAZZER_FUZZ set to a " + + "non-empty value."); + } + if (!serializer.allReadsValid()) { + extensionContext.publishReportEntry( + "Some files in the seed corpus do not match the fuzz target signature.\n" + + "This indicates that they were generated with a different signature and may cause " + + "issues reproducing previous findings."); + } + }); + } + + /** + * Used in regression mode to get test cases for the associated {@code testMethod} + * This will return a stream of files consisting of: + * <ul> + * <li>{@code resources/<classpath>/<testClass name>Inputs/*}</li> + * <li>{@code resources/<classpath>/<testClass name>Inputs/<testMethod name>/**}</li> + * </ul> + * Or the equivalent behavior on resources inside a jar file. + * <p> + * Note that the first {@code <testClass name>Inputs} path will not recursively search all + * directories but only gives files in that directory whereas the {@code <testMethod name>} + * directory is searched recursively. This allows for multiple tests to share inputs without + * needing to explicitly copy them into each test's directory. + * + * @param testClass the class of the test being run + * @param testMethod the test function being run + * @return a stream of findings files to use as inputs for the test function + */ + private Stream<Map.Entry<String, byte[]>> walkInputs(Class<?> testClass, Method testMethod) + throws IOException { + URL classInputsDirUrl = testClass.getResource(Utils.inputsDirectoryResourcePath(testClass)); + + if (classInputsDirUrl == null) { + return Stream.empty(); + } + URI classInputsDirUri; + try { + classInputsDirUri = classInputsDirUrl.toURI(); + } catch (URISyntaxException e) { + throw new IOException("Failed to open inputs resource directory: " + classInputsDirUrl, e); + } + if (classInputsDirUri.getScheme().equals("file")) { + // The test is executed from class files, which usually happens when run from inside an IDE. + Path classInputsPath = Paths.get(classInputsDirUri); + + return Stream.concat( + walkClassInputs(classInputsPath), walkTestInputs(classInputsPath, testMethod)); + + } else if (classInputsDirUri.getScheme().equals("jar")) { + FileSystem jar = FileSystems.newFileSystem(classInputsDirUri, new HashMap<>()); + // inputsDirUrl looks like this: + // file:/tmp/testdata/ExampleFuzzTest_deploy.jar!/com/code_intelligence/jazzer/junit/testdata/ExampleFuzzTestInputs + String pathInJar = + classInputsDirUrl.getFile().substring(classInputsDirUrl.getFile().indexOf('!') + 1); + + Path classPathInJar = jar.getPath(pathInJar); + + return Stream + .concat(walkClassInputs(classPathInJar), walkTestInputs(classPathInJar, testMethod)) + .onClose(() -> { + try { + jar.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } else { + throw new IOException( + "Unsupported protocol for inputs resource directory: " + classInputsDirUrl); + } + } + + /** + * Walks over the inputs for the method being tested, recurses into subdirectories + * @param classInputsPath the path of the class being tested, used as the base path where the test + * method's directory + * should be + * @param testMethod the method being tested + * @return a stream of all files under {@code <classInputsPath>/<testMethod name>} + * @throws IOException can be thrown by the underlying call to {@link Files#find} + */ + private static Stream<Map.Entry<String, byte[]>> walkTestInputs( + Path classInputsPath, Method testMethod) throws IOException { + Path testInputsPath = classInputsPath.resolve(testMethod.getName()); + try { + return walkInputsInPath(testInputsPath, Integer.MAX_VALUE); + } catch (NoSuchFileException e) { + return Stream.empty(); + } + } + + /** + * Walks over the inputs for the class being tested. Does not recurse into subdirectories + * @param path the path to search to files + * @return a stream of all files (without directories) within {@code path}. If {@code path} is not + * found, an empty + * stream is returned. + * @throws IOException can be thrown by the underlying call to {@link Files#find} + */ + private static Stream<Map.Entry<String, byte[]>> walkClassInputs(Path path) throws IOException { + try { + // using a depth of 1 will get all files within the given path but does not recurse into + // subdirectories + return walkInputsInPath(path, 1); + } catch (NoSuchFileException e) { + return Stream.empty(); + } + } + + /** + * Gets a sorted stream of all files (without directories) within under the given {@code path} + * @param path the path to walk + * @param depth the maximum depth of subdirectories to search from within {@code path}. 1 + * indicates it should return + * only the files directly in {@code path} and not search any of its subdirectories + * @return a stream of file name -> file contents as a raw byte array + * @throws IOException can be thrown by the call to {@link Files#find(Path, int, BiPredicate, + * FileVisitOption...)} + */ + private static Stream<Map.Entry<String, byte[]>> walkInputsInPath(Path path, int depth) + throws IOException { + // @ParameterTest automatically closes Streams and AutoCloseable instances. + // noinspection resource + return Files + .find(path, depth, + (fileOrDir, basicFileAttributes) + -> !basicFileAttributes.isDirectory(), + FileVisitOption.FOLLOW_LINKS) + // JUnit identifies individual runs of a `@ParameterizedTest` via their invocation number. + // In order to get reproducible behavior e.g. when trying to debug a particular input, all + // inputs thus have to be provided in deterministic order. + .sorted() + .map(file + -> new SimpleImmutableEntry<>( + file.getFileName().toString(), readAllBytesUnchecked(file))); + } + + private static byte[] readAllBytesUnchecked(Path path) { + try { + return Files.readAllBytes(path); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/junit/SeedSerializer.java b/src/main/java/com/code_intelligence/jazzer/junit/SeedSerializer.java new file mode 100644 index 00000000..0cebef2f --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/junit/SeedSerializer.java @@ -0,0 +1,144 @@ +/* + * 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.junit; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.autofuzz.Meta; +import com.code_intelligence.jazzer.driver.FuzzedDataProviderImpl; +import com.code_intelligence.jazzer.driver.Opt; +import com.code_intelligence.jazzer.mutation.ArgumentsMutator; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Method; +import java.util.Optional; + +interface SeedSerializer { + Object[] read(byte[] bytes); + default boolean allReadsValid() { + return true; + } + + // Implementations can assume that the argument array contains valid arguments for the method that + // this instance has been constructed for. + byte[] write(Object[] args) throws UnsupportedOperationException; + + /** + * Creates specialized {@link SeedSerializer} instances for the following method parameters: + * <ul> + * <li>{@code byte[]} + * <li>{@code FuzzDataProvider} + * <li>Any other types will attempt to be created using either Autofuzz or the experimental + * mutator framework if {@link Opt}'s {@code experimentalMutator} is set. + * </ul> + */ + static SeedSerializer of(Method method) { + if (method.getParameterCount() == 0) { + throw new IllegalArgumentException( + "Methods annotated with @FuzzTest must take at least one parameter"); + } + if (method.getParameterCount() == 1 && method.getParameterTypes()[0] == byte[].class) { + return new ByteArraySeedSerializer(); + } else if (method.getParameterCount() == 1 + && method.getParameterTypes()[0] == FuzzedDataProvider.class) { + return new FuzzedDataProviderSeedSerializer(); + } else { + Optional<ArgumentsMutator> argumentsMutator = + Opt.experimentalMutator ? ArgumentsMutator.forMethod(method) : Optional.empty(); + return argumentsMutator.<SeedSerializer>map(ArgumentsMutatorSeedSerializer::new) + .orElseGet(() -> new AutofuzzSeedSerializer(method)); + } + } +} + +final class ByteArraySeedSerializer implements SeedSerializer { + @Override + public Object[] read(byte[] bytes) { + return new Object[] {bytes}; + } + + @Override + public byte[] write(Object[] args) { + return (byte[]) args[0]; + } +} + +final class FuzzedDataProviderSeedSerializer implements SeedSerializer { + @Override + public Object[] read(byte[] bytes) { + return new Object[] {FuzzedDataProviderImpl.withJavaData(bytes)}; + } + + @Override + public byte[] write(Object[] args) throws UnsupportedOperationException { + // While we could get the underlying bytes, it's not possible to provide Java seeds for fuzz + // tests with a FuzzedDataProvider parameter. + throw new UnsupportedOperationException(); + } +} + +final class ArgumentsMutatorSeedSerializer implements SeedSerializer { + private final ArgumentsMutator mutator; + private boolean allReadsValid; + + public ArgumentsMutatorSeedSerializer(ArgumentsMutator mutator) { + this.mutator = mutator; + } + + @Override + public Object[] read(byte[] bytes) { + allReadsValid &= mutator.read(new ByteArrayInputStream(bytes)); + return mutator.getArguments(); + } + + @Override + public boolean allReadsValid() { + return allReadsValid; + } + + @Override + public byte[] write(Object[] args) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mutator.writeAny(out, args); + return out.toByteArray(); + } +} + +final class AutofuzzSeedSerializer implements SeedSerializer { + private final Meta meta; + private final Method method; + + public AutofuzzSeedSerializer(Method method) { + this.meta = new Meta(method.getDeclaringClass()); + this.method = method; + } + + @Override + public Object[] read(byte[] bytes) { + try (FuzzedDataProviderImpl data = FuzzedDataProviderImpl.withJavaData(bytes)) { + // The Autofuzz FuzzTarget uses data to construct an instance of the test class before + // it constructs the fuzz test arguments. We don't need the instance here, but still + // generate it as that mutates the FuzzedDataProvider state. + meta.consumeNonStatic(data, method.getDeclaringClass()); + return meta.consumeArguments(data, method, null); + } + } + + @Override + public byte[] write(Object[] args) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/junit/Utils.java b/src/main/java/com/code_intelligence/jazzer/junit/Utils.java new file mode 100644 index 00000000..4ab81cd1 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/junit/Utils.java @@ -0,0 +1,305 @@ +// Copyright 2022 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.junit; + +import static java.util.Arrays.stream; +import static java.util.Collections.newSetFromMap; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; +import static org.junit.jupiter.api.Named.named; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import com.code_intelligence.jazzer.utils.UnsafeProvider; +import com.code_intelligence.jazzer.utils.UnsafeUtils; +import java.io.File; +import java.io.IOException; +import java.lang.invoke.MethodType; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import org.junit.jupiter.params.provider.Arguments; + +class Utils { + /** + * Returns the resource path of the inputs directory for a given test class and method. The path + * will have the form + * {@code <class name>Inputs/<method name>} + */ + static String inputsDirectoryResourcePath(Class<?> testClass, Method testMethod) { + return testClass.getSimpleName() + "Inputs" + + "/" + testMethod.getName(); + } + + static String inputsDirectoryResourcePath(Class<?> testClass) { + return testClass.getSimpleName() + "Inputs"; + } + + /** + * Returns the file system path of the inputs corpus directory in the source tree, if it exists. + * The directory is created if it does not exist, but the test resource directory itself exists. + */ + static Optional<Path> inputsDirectorySourcePath( + Class<?> testClass, Method testMethod, Path baseDir) { + String inputsResourcePath = Utils.inputsDirectoryResourcePath(testClass, testMethod); + // Make the inputs resource path absolute. + if (!inputsResourcePath.startsWith("/")) { + String inputsPackage = testClass.getPackage().getName().replace('.', '/'); + inputsResourcePath = "/" + inputsPackage + "/" + inputsResourcePath; + } + + // Following the Maven directory layout, we look up the inputs directory under + // src/test/resources. This should be correct also for multi-module projects as JUnit is usually + // launched in the current module's root directory. + Path testResourcesDirectory = baseDir.resolve("src").resolve("test").resolve("resources"); + Path sourceInputsDirectory = testResourcesDirectory; + for (String segment : inputsResourcePath.split("/")) { + sourceInputsDirectory = sourceInputsDirectory.resolve(segment); + } + if (Files.isDirectory(sourceInputsDirectory)) { + return Optional.of(sourceInputsDirectory); + } + // If we can at least find the test resource directory, create the inputs directory. + if (!Files.isDirectory(testResourcesDirectory)) { + return Optional.empty(); + } + try { + return Optional.of(Files.createDirectories(sourceInputsDirectory)); + } catch (Exception e) { + return Optional.empty(); + } + } + + static Path generatedCorpusPath(Class<?> testClass, Method testMethod) { + return Paths.get(".cifuzz-corpus", testClass.getName(), testMethod.getName()); + } + + /** + * Returns a heuristic default value for jazzer.instrument based on the test class. + */ + static String getLegacyInstrumentationFilter(Class<?> testClass) { + // This is an extremely rough "implementation" of the public suffix list algorithm + // (https://publicsuffix.org/): It tries to guess the shortest prefix of the package name that + // isn't public. It doesn't use the actual list, but instead assumes that every root segment as + // well as "com.github" are public. Examples: + // - com.example.Test --> com.example.** + // - com.example.foobar.Test --> com.example.** + // - com.github.someones.repo.Test --> com.github.someones.** + String packageName = testClass.getPackage().getName(); + String[] packageSegments = packageName.split("\\."); + int numSegments = 2; + if (packageSegments.length > 2 && packageSegments[0].equals("com") + && packageSegments[1].equals("github")) { + numSegments = 3; + } + return Stream.concat(Arrays.stream(packageSegments).limit(numSegments), Stream.of("**")) + .collect(joining(".")); + } + + private static final Pattern CLASSPATH_SPLITTER = + Pattern.compile(Pattern.quote(File.pathSeparator)); + + /** + * Returns a heuristic default value for jazzer.instrument based on the files on the provided + * classpath. + */ + static Optional<String> getClassPathBasedInstrumentationFilter(String classPath) { + List<Path> includes = + CLASSPATH_SPLITTER.splitAsStream(classPath) + .map(Paths::get) + // We consider classpath entries that are directories rather than jar files to contain + // the classes of the current project rather than external dependencies. This is just a + // heuristic and breaks with build systems that package all classes in jar files, e.g. + // with Bazel. + .filter(Files::isDirectory) + .flatMap(root -> { + HashSet<Path> pkgs = new HashSet<>(); + try { + Files.walkFileTree(root, new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult preVisitDirectory( + Path dir, BasicFileAttributes basicFileAttributes) throws IOException { + try (Stream<Path> entries = Files.list(dir)) { + // If a directory contains a .class file, we add an include filter matching it + // and all subdirectories. + // Special case: If there is a class defined at the root, only the unnamed + // package is included, so continue with the traversal of subdirectories + // to discover additional includes. + if (entries.filter(path -> path.toString().endsWith(".class")) + .anyMatch(Files::isRegularFile)) { + Path pkgPath = root.relativize(dir); + pkgs.add(pkgPath); + if (pkgPath.toString().isEmpty()) { + return FileVisitResult.CONTINUE; + } else { + return FileVisitResult.SKIP_SUBTREE; + } + } + } + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + // This is only a best-effort heuristic anyway, ignore this directory. + return Stream.of(); + } + return pkgs.stream(); + }) + .distinct() + .collect(toList()); + if (includes.isEmpty()) { + return Optional.empty(); + } + return Optional.of( + includes.stream() + .map(Path::toString) + // For classes without a package, only include the unnamed package. + .map(path -> path.isEmpty() ? "*" : path.replace(File.separator, ".") + ".**") + .sorted() + // jazzer.instrument uses ',' as the separator. + .collect(joining(","))); + } + + private static final Pattern COVERAGE_AGENT_ARG = + Pattern.compile("-javaagent:.*(?:intellij-coverage-agent|jacoco).*"); + static boolean isCoverageAgentPresent() { + return ManagementFactory.getRuntimeMXBean().getInputArguments().stream().anyMatch( + s -> COVERAGE_AGENT_ARG.matcher(s).matches()); + } + + private static final boolean IS_FUZZING_ENV = + System.getenv("JAZZER_FUZZ") != null && !System.getenv("JAZZER_FUZZ").isEmpty(); + static boolean isFuzzing(ExtensionContext extensionContext) { + return IS_FUZZING_ENV || runFromCommandLine(extensionContext); + } + + static boolean runFromCommandLine(ExtensionContext extensionContext) { + return extensionContext.getConfigurationParameter("jazzer.internal.commandLine") + .map(Boolean::parseBoolean) + .orElse(false); + } + + /** + * Returns true if and only if the value is equal to "true", "1", or "yes" case-insensitively. + */ + static boolean permissivelyParseBoolean(String value) { + return value.equalsIgnoreCase("true") || value.equals("1") || value.equalsIgnoreCase("yes"); + } + + /** + * Convert the string to ISO 8601 (https://en.wikipedia.org/wiki/ISO_8601#Durations). We do not + * allow for duration units longer than hours, so we can always prepend PT. + */ + static long durationStringToSeconds(String duration) { + String isoDuration = + "PT" + duration.replace("sec", "s").replace("min", "m").replace("hr", "h").replace(" ", ""); + return Duration.parse(isoDuration).getSeconds(); + } + + /** + * Creates {@link Arguments} for a single invocation of a parameterized test that can be + * identified as having been created in this way by {@link #isMarkedInvocation}. + * + * @param displayName the display name to assign to every argument + */ + static Arguments getMarkedArguments(Method method, String displayName) { + return arguments(stream(method.getParameterTypes()) + .map(Utils::getMarkedInstance) + // Wrap in named as toString may crash on marked instances. + .map(arg -> named(displayName, arg)) + .toArray(Object[] ::new)); + } + + /** + * @return {@code true} if and only if the arguments for this test method invocation were created + * with {@link #getMarkedArguments} + */ + static boolean isMarkedInvocation(ReflectiveInvocationContext<Method> invocationContext) { + if (invocationContext.getArguments().stream().anyMatch(Utils::isMarkedInstance)) { + if (invocationContext.getArguments().stream().allMatch(Utils::isMarkedInstance)) { + return true; + } + throw new IllegalStateException( + "Some, but not all arguments were marked in invocation of " + invocationContext); + } else { + return false; + } + } + + private static final ClassValue<Object> uniqueInstanceCache = new ClassValue<Object>() { + @Override + protected Object computeValue(Class<?> clazz) { + return makeMarkedInstance(clazz); + } + }; + private static final Set<Object> uniqueInstances = newSetFromMap(new IdentityHashMap<>()); + + // Visible for testing. + static <T> T getMarkedInstance(Class<T> clazz) { + // makeMarkedInstance creates new classes, which is expensive and can cause the JVM to run out + // of metaspace. We thus cache the marked instances per class. + Object instance = uniqueInstanceCache.get(clazz); + uniqueInstances.add(instance); + return (T) instance; + } + + // Visible for testing. + static boolean isMarkedInstance(Object instance) { + return uniqueInstances.contains(instance); + } + + private static Object makeMarkedInstance(Class<?> clazz) { + if (clazz == Class.class) { + return new Object() {}.getClass(); + } + if (clazz.isArray()) { + return Array.newInstance(clazz.getComponentType(), 0); + } + if (clazz.isInterface()) { + return Proxy.newProxyInstance( + Utils.class.getClassLoader(), new Class[] {clazz}, (o, method, objects) -> null); + } + + if (clazz.isPrimitive()) { + clazz = MethodType.methodType(clazz).wrap().returnType(); + } else if (Modifier.isAbstract(clazz.getModifiers())) { + clazz = UnsafeUtils.defineAnonymousConcreteSubclass(clazz); + } + + try { + return clazz.cast(UnsafeProvider.getUnsafe().allocateInstance(clazz)); + } catch (InstantiationException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java new file mode 100644 index 00000000..fabda057 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java @@ -0,0 +1,232 @@ +/* + * 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.mutation; + +import static com.code_intelligence.jazzer.mutation.mutator.Mutators.validateAnnotationUsage; +import static com.code_intelligence.jazzer.mutation.support.InputStreamSupport.extendWithReadExactly; +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static com.code_intelligence.jazzer.mutation.support.StreamSupport.toArrayOrEmpty; +import static java.lang.String.format; +import static java.util.Arrays.stream; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; + +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators; +import com.code_intelligence.jazzer.mutation.combinator.ProductMutator; +import com.code_intelligence.jazzer.mutation.engine.SeededPseudoRandom; +import com.code_intelligence.jazzer.mutation.mutator.Mutators; +import com.code_intelligence.jazzer.mutation.support.InputStreamSupport.ReadExactlyInputStream; +import com.code_intelligence.jazzer.mutation.support.Preconditions; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Optional; + +public final class ArgumentsMutator { + private final Object instance; + private final Method method; + private final ProductMutator productMutator; + private Object[] arguments; + + /** + * True if the arguments array has already been passed to a user-provided function or exposed + * via {@link #getArguments()} without going through {@link ProductMutator#detach(Object[])}. + * In this case the arguments may have been modified externally, which interferes with mutation, + * or could have been stored in static state that would be affected by future mutations. + * Arguments should either be detached or not be reused after being exposed, which is enforced by + * this variable. + */ + private boolean argumentsExposed; + + private ArgumentsMutator(Object instance, Method method, ProductMutator productMutator) { + this.instance = instance; + this.method = method; + this.productMutator = productMutator; + } + + private static String prettyPrintMethod(Method method) { + return format("%s.%s(%s)", method.getDeclaringClass().getName(), method.getName(), + stream(method.getAnnotatedParameterTypes()).map(Object::toString).collect(joining(", "))); + } + + public static ArgumentsMutator forInstanceMethodOrThrow(Object instance, Method method) { + return forInstanceMethod(Mutators.newFactory(), instance, method) + .orElseThrow(() + -> new IllegalArgumentException( + "Failed to construct mutator for " + prettyPrintMethod(method))); + } + + public static ArgumentsMutator forStaticMethodOrThrow(Method method) { + return forStaticMethod(Mutators.newFactory(), method) + .orElseThrow(() + -> new IllegalArgumentException( + "Failed to construct mutator for " + prettyPrintMethod(method))); + } + + public static Optional<ArgumentsMutator> forMethod(Method method) { + return forMethod(Mutators.newFactory(), null, method); + } + + public static Optional<ArgumentsMutator> forInstanceMethod( + MutatorFactory mutatorFactory, Object instance, Method method) { + require(!isStatic(method), "method must not be static"); + requireNonNull(instance, "instance must not be null"); + require(method.getDeclaringClass().isInstance(instance), + format("instance is a %s, expected %s", instance.getClass(), method.getDeclaringClass())); + return forMethod(mutatorFactory, instance, method); + } + + public static Optional<ArgumentsMutator> forStaticMethod( + MutatorFactory mutatorFactory, Method method) { + require(isStatic(method), "method must be static"); + return forMethod(mutatorFactory, null, method); + } + + public static Optional<ArgumentsMutator> forMethod( + MutatorFactory mutatorFactory, Object instance, Method method) { + require(method.getParameterCount() > 0, "Can't fuzz method without parameters: " + method); + for (AnnotatedType parameter : method.getAnnotatedParameterTypes()) { + validateAnnotationUsage(parameter); + } + return toArrayOrEmpty( + stream(method.getAnnotatedParameterTypes()).map(mutatorFactory::tryCreate), + SerializingMutator<?>[] ::new) + .map(MutatorCombinators::mutateProduct) + .map(productMutator -> ArgumentsMutator.create(instance, method, productMutator)); + } + + private static ArgumentsMutator create( + Object instance, Method method, ProductMutator productMutator) { + method.setAccessible(true); + + return new ArgumentsMutator(instance, method, productMutator); + } + + private static boolean isStatic(Method method) { + return Modifier.isStatic(method.getModifiers()); + } + + /** + * @throws UncheckedIOException if the underlying InputStream throws + */ + public void crossOver(InputStream data1, InputStream data2, long seed) { + try { + Object[] objects1 = productMutator.readExclusive(data1); + Object[] objects2 = productMutator.readExclusive(data2); + PseudoRandom prng = new SeededPseudoRandom(seed); + arguments = productMutator.crossOver(objects1, objects2, prng); + argumentsExposed = false; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * @return if the given input stream was consumed exactly + * @throws UncheckedIOException if the underlying InputStream throws + */ + public boolean read(ByteArrayInputStream data) { + try { + ReadExactlyInputStream is = extendWithReadExactly(data); + arguments = productMutator.readExclusive(is); + argumentsExposed = false; + return is.isConsumedExactly(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * @throws UncheckedIOException if the underlying OutputStream throws + */ + public void write(OutputStream data) { + failIfArgumentsExposed(); + writeAny(data, arguments); + } + + /** + * @throws UncheckedIOException if the underlying OutputStream throws + */ + public void writeAny(OutputStream data, Object[] args) throws UncheckedIOException { + try { + productMutator.writeExclusive(args, data); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void init(long seed) { + init(new SeededPseudoRandom(seed)); + } + + void init(PseudoRandom prng) { + arguments = productMutator.init(prng); + argumentsExposed = false; + } + + public void mutate(long seed) { + mutate(new SeededPseudoRandom(seed)); + } + + void mutate(PseudoRandom prng) { + failIfArgumentsExposed(); + // TODO: Sometimes mutate the entire byte representation of the current value with libFuzzer's + // dictionary and TORC mutations. + productMutator.mutate(arguments, prng); + } + + public void invoke(boolean detach) throws Throwable { + Object[] invokeArguments; + if (detach) { + invokeArguments = productMutator.detach(arguments); + } else { + invokeArguments = arguments; + argumentsExposed = true; + } + try { + method.invoke(instance, invokeArguments); + } catch (IllegalAccessException e) { + throw new IllegalStateException("method should have been made accessible", e); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + public Object[] getArguments() { + argumentsExposed = true; + return arguments; + } + + @Override + public String toString() { + return "Arguments" + productMutator; + } + + private void failIfArgumentsExposed() { + Preconditions.check(!argumentsExposed, + "Arguments have previously been exposed to user-provided code without calling #detach and may have been modified"); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel new file mode 100644 index 00000000..9866c2c4 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel @@ -0,0 +1,16 @@ +java_library( + name = "mutation", + srcs = glob(["*.java"]), + visibility = [ + "//visibility:public", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/api", + "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", + "//src/main/java/com/code_intelligence/jazzer/mutation/api", + "//src/main/java/com/code_intelligence/jazzer/mutation/combinator", + "//src/main/java/com/code_intelligence/jazzer/mutation/engine", + "//src/main/java/com/code_intelligence/jazzer/mutation/mutator", + "//src/main/java/com/code_intelligence/jazzer/mutation/support", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/AppliesTo.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/AppliesTo.java new file mode 100644 index 00000000..4d4c4a89 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/AppliesTo.java @@ -0,0 +1,40 @@ +/* + * 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.mutation.annotation; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * A meta-annotation that limits the concrete types an annotation for type usages applies to. + */ +@Target(ANNOTATION_TYPE) +@Retention(RUNTIME) +public @interface AppliesTo { + /** + * The meta-annotated annotation can be applied to these classes. + */ + Class<?>[] value() default {}; + + /** + * The meta-annotated annotation can be applied to subclasses of these classes. + */ + Class<?>[] subClassesOf() default {}; +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/Ascii.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/Ascii.java new file mode 100644 index 00000000..190ada09 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/Ascii.java @@ -0,0 +1,28 @@ +/* + * 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.mutation.annotation; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Target(TYPE_USE) +@Retention(RUNTIME) +@AppliesTo(String.class) +public @interface Ascii {} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/BUILD.bazel new file mode 100644 index 00000000..6d6c4da9 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/BUILD.bazel @@ -0,0 +1,5 @@ +java_library( + name = "annotation", + srcs = glob(["*.java"]), + visibility = ["//visibility:public"], +) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/DoubleInRange.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/DoubleInRange.java new file mode 100644 index 00000000..27765879 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/DoubleInRange.java @@ -0,0 +1,32 @@ +/* + * 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.mutation.annotation; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Target(TYPE_USE) +@Retention(RUNTIME) +@AppliesTo({double.class, Double.class}) +public @interface DoubleInRange { + double min() default Double.NEGATIVE_INFINITY; + double max() default Double.POSITIVE_INFINITY; + boolean allowNaN() default true; +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/FloatInRange.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/FloatInRange.java new file mode 100644 index 00000000..ec54e026 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/FloatInRange.java @@ -0,0 +1,32 @@ +/* + * 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.mutation.annotation; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Target(TYPE_USE) +@Retention(RUNTIME) +@AppliesTo({float.class, Float.class}) +public @interface FloatInRange { + float min() default Float.NEGATIVE_INFINITY; + float max() default Float.POSITIVE_INFINITY; + boolean allowNaN() default true; +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/InRange.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/InRange.java new file mode 100644 index 00000000..a8dc8281 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/InRange.java @@ -0,0 +1,35 @@ +/* + * 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.mutation.annotation; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(TYPE_USE) +@Retention(RUNTIME) +@AppliesTo({byte.class, Byte.class, short.class, Short.class, int.class, Integer.class, long.class, + Long.class}) +public @interface InRange { + long min() default Long.MIN_VALUE; + + long max() default Long.MAX_VALUE; +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/NotNull.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/NotNull.java new file mode 100644 index 00000000..061eeffc --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/NotNull.java @@ -0,0 +1,29 @@ +/* + * 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.mutation.annotation; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Target({PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +@AppliesTo(subClassesOf = Object.class) +public @interface NotNull {} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithLength.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithLength.java new file mode 100644 index 00000000..7cee23df --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithLength.java @@ -0,0 +1,33 @@ +/* + * 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.mutation.annotation; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Target(TYPE_USE) +@Retention(RUNTIME) +@AppliesTo(byte[].class) +public @interface WithLength { + int min() default 0; + + int max() default 1000; +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithSize.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithSize.java new file mode 100644 index 00000000..32233f4b --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithSize.java @@ -0,0 +1,34 @@ +/* + * 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.mutation.annotation; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.List; +import java.util.Map; + +@Target(TYPE_USE) +@Retention(RUNTIME) +@AppliesTo({List.class, Map.class}) +public @interface WithSize { + int min() default 0; + + int max() default 1000; +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithUtf8Length.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithUtf8Length.java new file mode 100644 index 00000000..00b1f463 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/WithUtf8Length.java @@ -0,0 +1,43 @@ +/* + * 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.mutation.annotation; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * An annotation that applies to {@link String} to <strong>limit the length of the UTF-8 + * encoding</strong> of the string. In practical terms, this means that strings given this + * annotation will sometimes have a {@link String#length()} of less than + * {@code min} but will never exceed {@code max}. <p> Due to the fact that our String mutator is + * backed by the byte array mutator, it's difficult to know how many characters we'll get from the + * byte array we get from libfuzzer. Rather than reuse {@link WithLength} for strings which may give + * the impression that {@link String#length()} will return a value between {@code min} and {@code + * max}, we use this annotation to help make clear that the string consists of between + * {@code min} and {@code max} UTF-8 bytes, not necessarily (UTF-16) characters. + */ +@Target(TYPE_USE) +@Retention(RUNTIME) +@AppliesTo(String.class) +public @interface WithUtf8Length { + int min() default 0; + + int max() default 1000; +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/AnySource.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/AnySource.java new file mode 100644 index 00000000..9f802dfe --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/AnySource.java @@ -0,0 +1,40 @@ +/* + * 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.mutation.annotation.proto; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.code_intelligence.jazzer.mutation.annotation.AppliesTo; +import com.google.protobuf.Message; +import com.google.protobuf.Message.Builder; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Controls the mutations of {@link com.google.protobuf.Any} fields in messages of the annotated + * type as well as its recursive message fields. + */ +@Target(TYPE_USE) +@Retention(RUNTIME) +@AppliesTo(subClassesOf = {Message.class, Builder.class}) +public @interface AnySource { + /** + * A non-empty list of {@link Message}s to use for {@link com.google.protobuf.Any} fields. + */ + Class<? extends Message>[] value(); +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/BUILD.bazel new file mode 100644 index 00000000..8ef6863b --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/BUILD.bazel @@ -0,0 +1,21 @@ +java_library( + name = "proto", + srcs = glob(["*.java"]), + visibility = ["//visibility:public"], + deps = [ + ":protobuf_runtime_compile_only", + "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", + ], +) + +java_library( + name = "protobuf_runtime_compile_only", + # The proto mutator factory detects the presence of Protobuf at runtime and disables itself if + # it isn't found. Without something else bringing in the Protobuf runtime, there is no point in + # supporting proto mutations. + neverlink = True, + visibility = ["//src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto:__pkg__"], + exports = [ + "@com_google_protobuf_protobuf_java//jar", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/WithDefaultInstance.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/WithDefaultInstance.java new file mode 100644 index 00000000..e26d73a2 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto/WithDefaultInstance.java @@ -0,0 +1,43 @@ +/* + * 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.mutation.annotation.proto; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.code_intelligence.jazzer.mutation.annotation.AppliesTo; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.Message; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Provides a default instance to use as the base for mutations of the annotated {@link Message} or + * {@link DynamicMessage.Builder}. + */ +@Target(TYPE_USE) +@Retention(RUNTIME) +@AppliesTo(subClassesOf = {Message.class, Message.Builder.class}) +public @interface WithDefaultInstance { + /** + * The fully qualified name of a static method (e.g. + * {@code com.example.MyClass#getDefaultInstance}) with return type assignable to + * {@link com.google.protobuf.Message}, which returns a default instance that mutations should be + * based on. + */ + String value(); +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/api/BUILD.bazel new file mode 100644 index 00000000..cd5fe60e --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/BUILD.bazel @@ -0,0 +1,9 @@ +java_library( + name = "api", + srcs = glob(["*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/support", + "@com_google_errorprone_error_prone_annotations//jar", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/ChainedMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/ChainedMutatorFactory.java new file mode 100644 index 00000000..bf27e81b --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/ChainedMutatorFactory.java @@ -0,0 +1,48 @@ +/* + * 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.mutation.api; + +import static com.code_intelligence.jazzer.mutation.support.StreamSupport.findFirstPresent; +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; + +import com.google.errorprone.annotations.CheckReturnValue; +import java.lang.reflect.AnnotatedType; +import java.util.List; +import java.util.Optional; + +/** + * A {@link MutatorFactory} that delegates to the given factories in order. + */ +public final class ChainedMutatorFactory extends MutatorFactory { + private final List<MutatorFactory> factories; + + /** + * Creates a {@link MutatorFactory} that delegates to the given factories in order. + * + * @param factories a possibly empty collection of factories + */ + public ChainedMutatorFactory(MutatorFactory... factories) { + this.factories = unmodifiableList(asList(factories)); + } + + @Override + @CheckReturnValue + public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory parent) { + return findFirstPresent(factories.stream().map(factory -> factory.tryCreate(type, parent))); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/Debuggable.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/Debuggable.java new file mode 100644 index 00000000..df6f8288 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/Debuggable.java @@ -0,0 +1,46 @@ +/* + * 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.mutation.api; + +import static java.util.Collections.newSetFromMap; +import static java.util.Objects.requireNonNull; + +import java.util.IdentityHashMap; +import java.util.Set; +import java.util.function.Predicate; + +public interface Debuggable { + /** + * Returns a string representation of the object that is meant to be used to make assertions about + * its structure in tests. + * + * @param isInCycle evaluates to {@code true} if a cycle has been detected during recursive calls + * of this function. Must be called at most once with {@code this} as the single + * argument. Implementing classes that know that their current instance can never + * be contained in recursive substructures need not call this method. + */ + String toDebugString(Predicate<Debuggable> isInCycle); + + /** + * Returns a string representation of the given {@link Debuggable} that is meant to be used to + * make assertions about its structure in tests. + */ + static String getDebugString(Debuggable debuggable) { + Set<Debuggable> seen = newSetFromMap(new IdentityHashMap<>()); + return debuggable.toDebugString(child -> !seen.add(requireNonNull(child))); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/Detacher.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/Detacher.java new file mode 100644 index 00000000..d927e505 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/Detacher.java @@ -0,0 +1,44 @@ +/* + * 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.mutation.api; + +import com.google.errorprone.annotations.CheckReturnValue; + +/** + * Knows how to clone a {@code T} such that it shares no mutable state with the original. + */ +@FunctionalInterface +public interface Detacher<T> { + /** + * Returns an equal instance that shares no mutable state with {@code value}. + * + * <p>Implementations + * <ul> + * <li>MUST return an instance that {@link Object#equals(Object)} the argument; + * <li>MUST return an instance that cannot be used to mutate the state of the argument through + * its API (ignoring uses of {@link sun.misc.Unsafe}); + * <li>MUST return an instance that is not affected by any changes to the original value made + * by any mutator;</li> + * <li>MUST be accepted by mutator methods just like the original value;</li> + * <li>MAY return the argument itself if it is deeply immutable. + * </ul> + * + * @param value the instance to detach + * @return an equal instance that shares no mutable state with {@code value} + */ + @CheckReturnValue T detach(T value); +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/InPlaceMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/InPlaceMutator.java new file mode 100644 index 00000000..cf1b243d --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/InPlaceMutator.java @@ -0,0 +1,74 @@ +/* + * 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.mutation.api; + +/** + * Knows how to initialize and mutate (parts of) an existing object of type {@code T} in place and + * how to incorporate (cross over) parts of another object of the same type. + * + * <p>Certain types, such as immutable and primitive types, can not be mutated in place. For + * example, {@link java.util.List} can be mutated in place whereas {@link String} and {@code int} + * can't. In such cases, use {@link ValueMutator} instead. + * + * <p>Implementations + * <ul> + * <li>MAY weakly associate mutable state with the identity (not equality class) of objects they + * have been passed as arguments or returned from initialization functions; + * <li>MAY assume that they are only passed arguments that they have initialized or mutated;</li> + * <li>SHOULD use {@link com.code_intelligence.jazzer.mutation.support.WeakIdentityHashMap} for + * this purpose; + * <li>MUST otherwise be deeply immutable; + * <li>SHOULD override {@link Object#toString()} to return {@code + * Debuggable.getDebugString(this)}. + * </ul> + * + * @param <T> the reference type this mutator operates on + */ +public interface InPlaceMutator<T> extends Debuggable { + /** + * Implementations + * <ul> + * <li>MUST accept any mutable instance of {@code T}, not just those it creates itself. + * <li>SHOULD, when called repeatedly, initialize the object in ways that are likely to be + * distinct. + * </ul> + */ + void initInPlace(T reference, PseudoRandom prng); + + /** + * Implementations + * <ul> + * <li>MUST ensure that {@code reference} does not {@link Object#equals(Object)} the state it + * had prior to the call (if possible); + * <li>MUST accept any mutable instance of {@code T}, not just those it creates itself. + * <li>SHOULD, when called repeatedly, be able to eventually reach any valid state of the part + * of {@code T} governed by this mutator; + * </ul> + */ + void mutateInPlace(T reference, PseudoRandom prng); + + /** + * Implementations + * <ul> + * <li>MUST ensure that {@code reference} does not {@link Object#equals(Object)} the state it + * had prior to the call (if possible); + * <li>MUST accept any mutable instance of {@code T}, not just those it creates itself. + * <li>MUST NOT mutate {@code otherReference}</li> + * </ul> + */ + void crossOverInPlace(T reference, T otherReference, PseudoRandom prng); +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/MutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/MutatorFactory.java new file mode 100644 index 00000000..64771285 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/MutatorFactory.java @@ -0,0 +1,80 @@ +/* + * 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.mutation.api; + +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asAnnotatedType; +import static java.lang.String.format; + +import com.google.errorprone.annotations.CheckReturnValue; +import java.lang.reflect.AnnotatedType; +import java.util.Optional; + +/** + * Instances of this class are not required to be thread safe, but are generally lightweight and can + * thus be created as needed. + */ +public abstract class MutatorFactory { + public final boolean canMutate(AnnotatedType type) { + return tryCreate(type).isPresent(); + } + + public final <T> SerializingMutator<T> createOrThrow(Class<T> clazz) { + return (SerializingMutator<T>) createOrThrow(asAnnotatedType(clazz)); + } + + public final SerializingMutator<?> createOrThrow(AnnotatedType type) { + Optional<SerializingMutator<?>> maybeMutator = tryCreate(type); + require(maybeMutator.isPresent(), "Failed to create mutator for " + type); + return maybeMutator.get(); + } + + public final SerializingInPlaceMutator<?> createInPlaceOrThrow(AnnotatedType type) { + Optional<SerializingInPlaceMutator<?>> maybeMutator = tryCreateInPlace(type); + require(maybeMutator.isPresent(), "Failed to create mutator for " + type); + return maybeMutator.get(); + } + + /** + * Tries to create a mutator for {@code type} and, if successful, asserts that it is an instance + * of {@link SerializingInPlaceMutator}. + */ + public final Optional<SerializingInPlaceMutator<?>> tryCreateInPlace(AnnotatedType type) { + return tryCreate(type).map(mutator -> { + require(mutator instanceof InPlaceMutator<?>, + format("Mutator for %s is not in-place: %s", type, mutator.getClass())); + return (SerializingInPlaceMutator<?>) mutator; + }); + } + + @CheckReturnValue + public final Optional<SerializingMutator<?>> tryCreate(AnnotatedType type) { + return tryCreate(type, this); + } + + /** + * Attempt to create a {@link SerializingMutator} for the given type. + * + * @param type the type to mutate + * @param factory the factory to use when creating submutators + * @return a {@link SerializingMutator} for the given {@code type}, or {@link Optional#empty()} + * if this factory can't create such mutators + */ + @CheckReturnValue + public abstract Optional<SerializingMutator<?>> tryCreate( + AnnotatedType type, MutatorFactory factory); +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/PseudoRandom.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/PseudoRandom.java new file mode 100644 index 00000000..3755a7b1 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/PseudoRandom.java @@ -0,0 +1,136 @@ +/* + * 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.mutation.api; + +import com.google.errorprone.annotations.DoNotMock; +import java.util.List; +import java.util.function.Supplier; + +@DoNotMock("Use TestSupport#mockPseudoRandom instead") +public interface PseudoRandom { + /** + * @return a uniformly random {@code boolean} + */ + boolean choice(); + + /** + * @return a {@code boolean} that is {@code true} with probability {@code 1/inverseFrequencyTrue} + */ + boolean trueInOneOutOf(int inverseFrequencyTrue); + + /** + * @throws IllegalArgumentException if {@code array.length == 0} + * @return an element from the given array at uniformly random index + */ + <T> T pickIn(T[] array); + + /** + * @throws IllegalArgumentException if {@code array.length == 0} + * @return an element from the given List at uniformly random index + */ + <T> T pickIn(List<T> array); + + /** + * @throws IllegalArgumentException if {@code array.length == 0} + * @return a uniformly random index valid for the given array + */ + <T> int indexIn(T[] array); + + /** + * @throws IllegalArgumentException if {@code list.size() == 0} + * @return a uniformly random index valid for the given list + */ + <T> int indexIn(List<T> list); + + /** + * Prefer {@link #indexIn(Object[])} and {@link #indexIn(List)}. + * + * @throws IllegalArgumentException if {@code range < 1} + * @return a uniformly random index in the range {@code [0, range-1]} + */ + int indexIn(int range); + + /** + * @throws IllegalArgumentException if {@code array.length < 2} + * @return a uniformly random index valid for the given array and different from + * {@code currentIndex} + */ + <T> int otherIndexIn(T[] array, int currentIndex); + + /** + * @throws IllegalArgumentException if {@code length < 2} + * @return a uniformly random {@code int} in the closed range {@code [0, length)} that is + * different from {@code currentIndex} + */ + int otherIndexIn(int range, int currentIndex); + + /** + * @return a uniformly random {@code int} in the closed range + * {@code [lowerInclusive, upperInclusive]}. + */ + int closedRange(int lowerInclusive, int upperInclusive); + + /** + * @return a uniformly random {@code long} in the closed range + * {@code [lowerInclusive, upperInclusive]}. + */ + long closedRange(long lowerInclusive, long upperInclusive); + + /** + * @return a uniformly random {@code float} in the closed range + * {@code [lowerInclusive, upperInclusive]}. + */ + float closedRange(float lowerInclusive, float upperInclusive); + + /** + * @return a uniformly random {@code double} in the closed range + * {@code [lowerInclusive, upperInclusive]}. + */ + double closedRange(double lowerInclusive, double upperInclusive); + + /** + * @return a random value in the closed range [0, upperInclusive] that is heavily biased towards + * being small + */ + int closedRangeBiasedTowardsSmall(int upperInclusive); + + /** + * @return a random value in the closed range [lowerInclusive, upperInclusive] that is heavily + * biased towards being small + */ + int closedRangeBiasedTowardsSmall(int lowerInclusive, int upperInclusive); + + /** + * Fills the given array with random bytes. + */ + void bytes(byte[] bytes); + + /** + * Use the given supplier to produce a value with probability {@code 1/inverseSupplierFrequency}, + * otherwise randomly return one of the given values. + * + * @return value produced by the supplier or one of the given values + */ + <T> T pickValue(T value, T otherValue, Supplier<T> supplier, int inverseSupplierFrequency); + + /** + * Returns a pseudorandom {@code long} value. + * + * @return a pseudorandom {@code long} value + */ + long nextLong(); +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/Serializer.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/Serializer.java new file mode 100644 index 00000000..b948b177 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/Serializer.java @@ -0,0 +1,116 @@ +/* + * 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.mutation.api; + +import static com.code_intelligence.jazzer.mutation.support.InputStreamSupport.extendWithZeros; + +import com.google.errorprone.annotations.CheckReturnValue; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Serializes and deserializes values of type {@code T>} to and from (in-memory or on disk) corpus + * entries. + * + * <p>Binary representations must by default be self-delimiting. For variable-length types, the + * {@link #readExclusive(InputStream)} and {@link #writeExclusive(Object, OutputStream)} methods can + * optionally be overriden to implement more compact representations that align with existing binary + * corpus entries. For example, a {@code Serializer<byte[]>} could implement these optional methods + * to read and write the raw bytes without preceding length information whenever it is used in an + * already delimited context. + */ +public interface Serializer<T> extends Detacher<T> { + /** + * Reads a {@code T} from an endless stream that is eventually 0. + * + * <p>Implementations + * <ul> + * <li>MUST not attempt to consume the entire stream; + * <li>MUST return a valid {@code T} and not throw for any (even garbage) stream; + * <li>SHOULD short-circuit the creation of nested structures upon reading null bytes. + * </ul> + * + * @param in an endless stream that eventually only reads null bytes + * @return a {@code T} constructed from the bytes read + * @throws IOException declared, but must not be thrown by implementations unless methods called + * on {@code in} do + */ + @CheckReturnValue T read(DataInputStream in) throws IOException; + + /** + * Writes a {@code T} to a stream in such a way that an equal object can be recovered from the + * written bytes via {@link #read(DataInputStream)}. + * + * <p>Since {@link #read(DataInputStream)} is called with an endless stream, the binary + * representation MUST be self-delimiting. For example, when writing out a list, first write its + * length. + * + * @param value the value to write + * @param out the stream to write to + * @throws IOException declared, but must not be thrown by implementations unless methods called + * on {@code out} do + */ + void write(T value, DataOutputStream out) throws IOException; + + /** + * Reads a {@code T} from a finite stream, potentially using a simpler representation than that + * read by {@link #read(DataInputStream)}. + * + * <p>The default implementations call extends the stream with null bytes and then calls + * {@link #read(DataInputStream)}. + * + * <p>Implementations + * <ul> + * <li>MUST return a valid {@code T} and not throw for any (even garbage) stream; + * <li>SHOULD short-circuit the creation of nested structures upon reading null bytes; + * <li>SHOULD naturally consume the entire stream. + * </ul> + * + * @param in a finite stream + * @return a {@code T} constructed from the bytes read + * @throws IOException declared, but must not be thrown by implementations unless methods called + * on {@code in} do + */ + @CheckReturnValue + default T readExclusive(InputStream in) throws IOException { + return read(new DataInputStream(extendWithZeros(in))); + } + + /** + * Writes a {@code T} to a stream in such a way that an equal object can be recovered from the + * written bytes via {@link #readExclusive(InputStream)}. + * + * <p>The default implementations calls through to {@link #read(DataInputStream)} and should only + * be overriden if {@link #readExclusive(InputStream)} is. + * + * <p>As opposed to {@link #read(DataInputStream)}, {@link #readExclusive(InputStream)} is called + * with a finite stream. The binary representation of a {@code T} value thus does not have to be + * self-delimiting, which can allow for simpler representations. For example, a {@code byte[]} can + * be written to the stream without prepending its length. + * + * @param value the value to write + * @param out the stream to write to + * @throws IOException declared, but must not be thrown by implementations unless methods called + * on {@code out} do + */ + default void writeExclusive(T value, OutputStream out) throws IOException { + write(value, new DataOutputStream(out)); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/SerializingInPlaceMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/SerializingInPlaceMutator.java new file mode 100644 index 00000000..b7bc4d4c --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/SerializingInPlaceMutator.java @@ -0,0 +1,76 @@ +/* + * 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.mutation.api; + +import static com.code_intelligence.jazzer.mutation.support.ExceptionSupport.asUnchecked; + +import com.google.errorprone.annotations.ForOverride; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Combines an {@link InPlaceMutator} with a {@link Serializer} for objects of type {@code T}. + * + * <p>If {@code T} can't be mutated in place, implement {@link SerializingMutator} instead. + * + * <p>Implementing classes SHOULD be declared final. + */ +public abstract class SerializingInPlaceMutator<T> + extends SerializingMutator<T> implements InPlaceMutator<T> { + // ByteArrayInputStream#close is documented as being a no-op, so it is safe to reuse an instance + // here. + // TODO: Introduce a dedicated empty InputStream implementation. + private static final InputStream emptyInputStream = new ByteArrayInputStream(new byte[0]); + + /** + * Constructs a default instance of {@code T}. + * + * <p>The returned value is immediately passed to {@link #initInPlace(Object, PseudoRandom)}. + * + * <p>Implementing classes SHOULD provide a more efficient implementation. + * + * @return a default instance of {@code T} + */ + @ForOverride + protected T makeDefaultInstance() { + try { + return readExclusive(emptyInputStream); + } catch (IOException e) { + throw asUnchecked(e); + } + } + + @Override + public final T init(PseudoRandom prng) { + T value = makeDefaultInstance(); + initInPlace(value, prng); + return value; + } + + @Override + public final T mutate(T value, PseudoRandom prng) { + mutateInPlace(value, prng); + return value; + } + + @Override + public final T crossOver(T value, T otherValue, PseudoRandom prng) { + crossOverInPlace(value, otherValue, prng); + return value; + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/SerializingMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/SerializingMutator.java new file mode 100644 index 00000000..58b2a49b --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/SerializingMutator.java @@ -0,0 +1,35 @@ +/* + * 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.mutation.api; + +import com.google.errorprone.annotations.DoNotMock; + +/** + * Combines a {@link ValueMutator} with a {@link Serializer} for objects of type {@code T}. + * + * <p>Implementing classes SHOULD be declared final. + * + * <p>This is the default fully-featured mutator type. If {@code T} can be mutated fully in place, + * consider implementing the more versatile {@link SerializingInPlaceMutator} instead. + */ +@DoNotMock("Use TestSupport#mockMutator instead") +public abstract class SerializingMutator<T> implements Serializer<T>, ValueMutator<T> { + @Override + public final String toString() { + return Debuggable.getDebugString(this); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/api/ValueMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/api/ValueMutator.java new file mode 100644 index 00000000..aa2b551e --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/api/ValueMutator.java @@ -0,0 +1,75 @@ +/* + * 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.mutation.api; + +import com.google.errorprone.annotations.CheckReturnValue; + +/** + * Knows how to initialize and mutate objects of type {@code T} and how to incorporate + * (cross over) parts of another object of the same type. + * + * <p>Certain types can be mutated fully in place. In such cases, prefer implementing the more + * versatile {@link InPlaceMutator} instead. + * + * <p>Implementations + * <ul> + * <li>MAY weakly associate mutable state with the identity (not equality class) of objects they + * have been passed as arguments or returned from initialization functions; + * <li>MAY assume that they are only passed arguments that they have initialized or mutated;</li> + * <li>SHOULD use {@link com.code_intelligence.jazzer.mutation.support.WeakIdentityHashMap} for + * this purpose; + * <li>MUST otherwise be deeply immutable; + * <li>SHOULD override {@link Object#toString()} to return {@code + * Debuggable.getDebugString(this)}. + * </ul> + * + * @param <T> the type this mutator operates on + */ +public interface ValueMutator<T> extends Debuggable { + /** + * Implementations + * <ul> + * <li>SHOULD, when called repeatedly, return a low amount of duplicates. + * </ul> + * + * @return an instance of {@code T} + */ + @CheckReturnValue T init(PseudoRandom prng); + + /** + * Implementations + * <ul> + * <li>MUST return a value that does not {@link Object#equals(Object)} the argument (if + * possible); + * <li>SHOULD, when called repeatedly, be able to eventually return any valid value of + * type {@code T}; + * <li>MAY mutate the argument. + * </ul> + */ + @CheckReturnValue T mutate(T value, PseudoRandom prng); + + /** + * Implementations + * <ul> + * <li>MUST return a value that does not {@link Object#equals(Object)} the arguments (if + * possible); + * <li>MAY mutate {@code value}. + * <li>MUST NOT mutate {@code otherValue}. + * </ul> + */ + @CheckReturnValue T crossOver(T value, T otherValue, PseudoRandom prng); +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/combinator/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/BUILD.bazel new file mode 100644 index 00000000..639a3d09 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/BUILD.bazel @@ -0,0 +1,11 @@ +java_library( + name = "combinator", + srcs = glob(["*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/api", + "//src/main/java/com/code_intelligence/jazzer/mutation/support", + "@com_github_jhalterman_typetools//:typetools", + "@com_google_errorprone_error_prone_type_annotations//jar", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/combinator/MutatorCombinators.java b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/MutatorCombinators.java new file mode 100644 index 00000000..18fa0288 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/MutatorCombinators.java @@ -0,0 +1,516 @@ +/* + * 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.mutation.combinator; + +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static com.code_intelligence.jazzer.mutation.support.Preconditions.requireNonNullElements; +import static java.util.Arrays.stream; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; + +import com.code_intelligence.jazzer.mutation.api.Debuggable; +import com.code_intelligence.jazzer.mutation.api.InPlaceMutator; +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.Serializer; +import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.google.errorprone.annotations.ImmutableTypeParameter; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.ToIntFunction; +import net.jodah.typetools.TypeResolver; + +public final class MutatorCombinators { + // Inverse frequency in which value mutator should be used in cross over. + private final static int INVERSE_PICK_VALUE_SUPPLIER_FREQUENCY = 100; + + private MutatorCombinators() {} + + public static <T, R> InPlaceMutator<T> mutateProperty( + Function<T, R> getter, SerializingMutator<R> mutator, BiConsumer<T, R> setter) { + requireNonNull(getter); + requireNonNull(mutator); + requireNonNull(setter); + return new InPlaceMutator<T>() { + @Override + public void initInPlace(T reference, PseudoRandom prng) { + setter.accept(reference, mutator.init(prng)); + } + + @Override + public void mutateInPlace(T reference, PseudoRandom prng) { + setter.accept(reference, mutator.mutate(getter.apply(reference), prng)); + } + + @Override + public void crossOverInPlace(T reference, T otherReference, PseudoRandom prng) { + // Most of the time cross over of properties should use one of the + // given values and only seldom use the property type specific cross + // over function. Other mutator combinators delegate to this one and + // don't cross over values themselves. + R referenceValue = getter.apply(reference); + R otherReferenceValue = getter.apply(otherReference); + R crossedOver = prng.pickValue(referenceValue, otherReferenceValue, + () + -> mutator.crossOver(referenceValue, otherReferenceValue, prng), + INVERSE_PICK_VALUE_SUPPLIER_FREQUENCY); + if (crossedOver == otherReferenceValue) { + // If otherReference was picked, it needs to be detached as mutating + // it is prohibited in cross over. + crossedOver = mutator.detach(crossedOver); + } + setter.accept(reference, crossedOver); + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + Class<?> owningType = + TypeResolver.resolveRawArguments(Function.class, getter.getClass())[0]; + return owningType.getSimpleName() + "." + mutator.toDebugString(isInCycle); + } + + @Override + public String toString() { + return Debuggable.getDebugString(this); + } + }; + } + + public static <T, R> InPlaceMutator<T> mutateViaView( + Function<T, R> map, InPlaceMutator<R> mutator) { + requireNonNull(map); + requireNonNull(mutator); + return new InPlaceMutator<T>() { + @Override + public void initInPlace(T reference, PseudoRandom prng) { + mutator.initInPlace(map.apply(reference), prng); + } + + @Override + public void mutateInPlace(T reference, PseudoRandom prng) { + mutator.mutateInPlace(map.apply(reference), prng); + } + + @Override + public void crossOverInPlace(T reference, T otherReference, PseudoRandom prng) { + mutator.crossOverInPlace(map.apply(reference), map.apply(otherReference), prng); + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + Class<?> owningType = TypeResolver.resolveRawArguments(Function.class, map.getClass())[0]; + return owningType.getSimpleName() + " via " + mutator.toDebugString(isInCycle); + } + + @Override + public String toString() { + return Debuggable.getDebugString(this); + } + }; + } + + /** + * Combines multiple in-place mutators for different parts of a {@code T} into one that picks one + * at random whenever it mutates. + * + * <p>Calling this method with no arguments returns a no-op mutator that may decrease fuzzing + * efficiency. + */ + @SafeVarargs + public static <T> InPlaceMutator<T> combine(InPlaceMutator<T>... partialMutators) { + requireNonNullElements(partialMutators); + if (partialMutators.length == 0) { + return new InPlaceMutator<T>() { + @Override + public void initInPlace(T reference, PseudoRandom prng) {} + + @Override + public void mutateInPlace(T reference, PseudoRandom prng) {} + + @Override + public void crossOverInPlace(T reference, T otherReference, PseudoRandom prng) {} + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return "{<empty>}"; + } + + @Override + public String toString() { + return Debuggable.getDebugString(this); + } + }; + } + + final InPlaceMutator<T>[] mutators = Arrays.copyOf(partialMutators, partialMutators.length); + return new InPlaceMutator<T>() { + @Override + public void initInPlace(T reference, PseudoRandom prng) { + for (InPlaceMutator<T> mutator : mutators) { + mutator.initInPlace(reference, prng); + } + } + + @Override + public void mutateInPlace(T reference, PseudoRandom prng) { + mutators[prng.indexIn(mutators)].mutateInPlace(reference, prng); + } + + @Override + public void crossOverInPlace(T reference, T otherReference, PseudoRandom prng) { + for (InPlaceMutator<T> mutator : mutators) { + mutator.crossOverInPlace(reference, otherReference, prng); + } + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return stream(mutators) + .map(mutator -> mutator.toDebugString(isInCycle)) + .collect(joining(", ", "{", "}")); + } + + @Override + public String toString() { + return Debuggable.getDebugString(this); + } + }; + } + + public static <T, R> SerializingMutator<R> mutateThenMap( + SerializingMutator<T> mutator, Function<T, R> map, Function<R, T> inverse) { + return new PostComposedMutator<T, R>(mutator, map, inverse) {}; + } + + public static <T, R> SerializingMutator<R> mutateThenMap(SerializingMutator<T> mutator, + Function<T, R> map, Function<R, T> inverse, Function<Predicate<Debuggable>, String> debug) { + return new PostComposedMutator<T, R>(mutator, map, inverse) { + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return debug.apply(isInCycle); + } + }; + } + + public static <T, @ImmutableTypeParameter R> SerializingMutator<R> mutateThenMapToImmutable( + SerializingMutator<T> mutator, Function<T, R> map, Function<R, T> inverse) { + return new PostComposedMutator<T, R>(mutator, map, inverse) { + @Override + public R detach(R value) { + return value; + } + }; + } + + public static <T, @ImmutableTypeParameter R> SerializingMutator<R> mutateThenMapToImmutable( + SerializingMutator<T> mutator, Function<T, R> map, Function<R, T> inverse, + Function<Predicate<Debuggable>, String> debug) { + return new PostComposedMutator<T, R>(mutator, map, inverse) { + @Override + public R detach(R value) { + return value; + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return debug.apply(isInCycle); + } + }; + } + + public static SerializingMutator<Integer> mutateIndices(int length) { + require(length > 1, "There should be at least two indices to choose from"); + return new SerializingMutator<Integer>() { + @Override + public Integer read(DataInputStream in) throws IOException { + return Math.floorMod(in.readInt(), length); + } + + @Override + public void write(Integer value, DataOutputStream out) throws IOException { + out.writeInt(value); + } + + @Override + public Integer detach(Integer value) { + return value; + } + + @Override + public Integer init(PseudoRandom prng) { + return prng.closedRange(0, length - 1); + } + + @Override + public Integer mutate(Integer value, PseudoRandom prng) { + return prng.otherIndexIn(length, value); + } + + @Override + public Integer crossOver(Integer value, Integer otherValue, PseudoRandom prng) { + return prng.choice() ? value : otherValue; + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return "mutateIndices(" + length + ")"; + } + }; + } + + /** + * Combines multiple mutators for potentially different types into one that mutates an + * {@code Object[]} containing one instance per mutator. + */ + @SuppressWarnings("rawtypes") + public static ProductMutator mutateProduct(SerializingMutator... mutators) { + return new ProductMutator(mutators); + } + + /** + * Mutates a sum type (e.g. a Protobuf oneof) in place, preferring to mutate the current state + * but occasionally switching to a different state. + * @param getState a function that returns the current state of the sum type as an index into + * {@code perStateMutators}, or -1 if the state is indeterminate. + * @param perStateMutators the mutators for each state + * @return a mutator that mutates the sum type in place + */ + @SafeVarargs + public static <T> InPlaceMutator<T> mutateSumInPlace( + ToIntFunction<T> getState, InPlaceMutator<T>... perStateMutators) { + final InPlaceMutator<T>[] mutators = Arrays.copyOf(perStateMutators, perStateMutators.length); + return new InPlaceMutator<T>() { + @Override + public void initInPlace(T reference, PseudoRandom prng) { + mutators[prng.indexIn(mutators)].initInPlace(reference, prng); + } + + @Override + public void mutateInPlace(T reference, PseudoRandom prng) { + int currentState = getState.applyAsInt(reference); + if (currentState == -1) { + // The value is in an indeterminate state, initialize it. + initInPlace(reference, prng); + } else if (prng.trueInOneOutOf(100) && mutators.length > 1) { + // Initialize to a different state. + mutators[prng.otherIndexIn(mutators, currentState)].initInPlace(reference, prng); + } else { + // Mutate within the current state. + mutators[currentState].mutateInPlace(reference, prng); + } + } + + @Override + public void crossOverInPlace(T reference, T otherReference, PseudoRandom prng) { + // Try to cross over in current state and leave state changes to the mutate step. + int currentState = getState.applyAsInt(reference); + int otherState = getState.applyAsInt(otherReference); + if (currentState == -1) { + // If reference is not initialized to a concrete state yet, try to do so in + // the state of other reference, as that's at least some progress. + if (otherState == -1) { + // If both states are indeterminate, cross over can not be performed. + return; + } + mutators[otherState].initInPlace(reference, prng); + } else if (currentState == otherState) { + mutators[currentState].crossOverInPlace(reference, otherReference, prng); + } + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return stream(mutators) + .map(mutator -> mutator.toDebugString(isInCycle)) + .collect(joining(" | ")); + } + }; + } + + /** + * @return a mutator that behaves identically to the provided one except that its {@link + * InPlaceMutator#initInPlace(Object, PseudoRandom)} is a no-op + */ + public static <T> InPlaceMutator<T> withoutInit(InPlaceMutator<T> mutator) { + return new InPlaceMutator<T>() { + @Override + public void initInPlace(T reference, PseudoRandom prng) { + // Intentionally left empty. + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return "WithoutInit(" + mutator.toDebugString(isInCycle) + ")"; + } + + @Override + public void mutateInPlace(T reference, PseudoRandom prng) { + mutator.mutateInPlace(reference, prng); + } + + @Override + public void crossOverInPlace(T reference, T otherReference, PseudoRandom prng) { + mutator.crossOverInPlace(reference, otherReference, prng); + } + }; + } + + /** + * Constructs a mutator that always returns the provided fixed value. + * + * <p>Note: This mutator explicitly breaks the contract of the init and mutate methods. Use + * sparingly as it may harm the overall effectivity of the mutator. + */ + public static <@ImmutableTypeParameter T> SerializingMutator<T> fixedValue(T value) { + return new SerializingMutator<T>() { + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return "FixedValue(" + value + ")"; + } + + @Override + public T read(DataInputStream in) { + return value; + } + + @Override + public void write(T value, DataOutputStream out) {} + + @Override + public T detach(T value) { + return value; + } + + @Override + public T init(PseudoRandom prng) { + return value; + } + + @Override + public T mutate(T value, PseudoRandom prng) { + return value; + } + + @Override + public T crossOver(T value, T otherValue, PseudoRandom prng) { + return value; + } + }; + } + + /** + * Assembles the parameters into a full implementation of {@link SerializingInPlaceMutator<T>}: + * + * @param registerSelf a callback that will receive the uninitialized mutator instance + * before {@code lazyMutator} is invoked. For simple cases this can + * just do nothing, but it is needed to implement mutators for + * structures that are self-referential (e.g. Protobuf message A having + * a field of type A). + * @param makeDefaultInstance constructs a mutable default instance of {@code T} + * @param serializer implementation of the {@link Serializer<T>} part + * @param lazyMutator supplies the implementation of the {@link InPlaceMutator<T>} part. + * This is guaranteed to be invoked exactly once and only after + * {@code registerSelf}. + */ + public static <T> SerializingInPlaceMutator<T> assemble( + Consumer<SerializingInPlaceMutator<T>> registerSelf, Supplier<T> makeDefaultInstance, + Serializer<T> serializer, Supplier<InPlaceMutator<T>> lazyMutator) { + return new DelegatingSerializingInPlaceMutator<>( + registerSelf, makeDefaultInstance, serializer, lazyMutator); + } + + private static class DelegatingSerializingInPlaceMutator<T> extends SerializingInPlaceMutator<T> { + private final Supplier<T> makeDefaultInstance; + private final Serializer<T> serializer; + private final InPlaceMutator<T> mutator; + + private DelegatingSerializingInPlaceMutator(Consumer<SerializingInPlaceMutator<T>> registerSelf, + Supplier<T> makeDefaultInstance, Serializer<T> serializer, + Supplier<InPlaceMutator<T>> lazyMutator) { + requireNonNull(makeDefaultInstance); + requireNonNull(serializer); + + registerSelf.accept(this); + this.makeDefaultInstance = makeDefaultInstance; + this.serializer = serializer; + this.mutator = lazyMutator.get(); + } + + @Override + public void initInPlace(T reference, PseudoRandom prng) { + mutator.initInPlace(reference, prng); + } + + @Override + public void mutateInPlace(T reference, PseudoRandom prng) { + mutator.mutateInPlace(reference, prng); + } + + @Override + public void crossOverInPlace(T reference, T otherReference, PseudoRandom prng) { + mutator.crossOverInPlace(reference, otherReference, prng); + } + + @Override + protected T makeDefaultInstance() { + return makeDefaultInstance.get(); + } + + @Override + public T read(DataInputStream in) throws IOException { + return serializer.read(in); + } + + @Override + public void write(T value, DataOutputStream out) throws IOException { + serializer.write(value, out); + } + + @Override + public T readExclusive(InputStream in) throws IOException { + return serializer.readExclusive(in); + } + + @Override + public void writeExclusive(T value, OutputStream out) throws IOException { + serializer.writeExclusive(value, out); + } + + @Override + public T detach(T value) { + return serializer.detach(value); + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + if (isInCycle.test(this)) { + return "(cycle)"; + } else { + return mutator.toDebugString(isInCycle); + } + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/combinator/PostComposedMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/PostComposedMutator.java new file mode 100644 index 00000000..ae8f97cc --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/PostComposedMutator.java @@ -0,0 +1,89 @@ +/* + * 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.mutation.combinator; + +import static java.util.Objects.requireNonNull; + +import com.code_intelligence.jazzer.mutation.api.Debuggable; +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.function.Function; +import java.util.function.Predicate; +import net.jodah.typetools.TypeResolver; + +abstract class PostComposedMutator<T, R> extends SerializingMutator<R> { + private final SerializingMutator<T> mutator; + private final Function<T, R> map; + private final Function<R, T> inverse; + + PostComposedMutator(SerializingMutator<T> mutator, Function<T, R> map, Function<R, T> inverse) { + this.mutator = requireNonNull(mutator); + this.map = requireNonNull(map); + this.inverse = requireNonNull(inverse); + } + + @Override + public R detach(R value) { + return map.apply(mutator.detach(inverse.apply(value))); + } + + @Override + public final R init(PseudoRandom prng) { + return map.apply(mutator.init(prng)); + } + + @Override + public final R mutate(R value, PseudoRandom prng) { + return map.apply(mutator.mutate(inverse.apply(value), prng)); + } + + @Override + public R crossOver(R value, R otherValue, PseudoRandom prng) { + return map.apply(mutator.crossOver(inverse.apply(value), inverse.apply(otherValue), prng)); + } + + @Override + public final R read(DataInputStream in) throws IOException { + return map.apply(mutator.read(in)); + } + + @Override + public final void write(R value, DataOutputStream out) throws IOException { + mutator.write(inverse.apply(value), out); + } + + @Override + public final R readExclusive(InputStream in) throws IOException { + return map.apply(mutator.readExclusive(in)); + } + + @Override + public final void writeExclusive(R value, OutputStream out) throws IOException { + mutator.writeExclusive(inverse.apply(value), out); + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + Class<?> returnType = TypeResolver.resolveRawArguments(Function.class, map.getClass())[1]; + return mutator.toDebugString(isInCycle) + " -> " + returnType.getSimpleName(); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/combinator/ProductMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/ProductMutator.java new file mode 100644 index 00000000..9057fd35 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/ProductMutator.java @@ -0,0 +1,138 @@ +/* + * 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.mutation.combinator; + +import static com.code_intelligence.jazzer.mutation.support.InputStreamSupport.extendWithZeros; +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static com.code_intelligence.jazzer.mutation.support.Preconditions.requireNonNullElements; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; + +import com.code_intelligence.jazzer.mutation.api.Debuggable; +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.function.Predicate; + +@SuppressWarnings({"unchecked", "rawtypes"}) +public final class ProductMutator extends SerializingInPlaceMutator<Object[]> { + // Inverse frequency in which product type mutators should be used in cross over. + private final static int INVERSE_PICK_VALUE_SUPPLIER_FREQUENCY = 100; + + private final SerializingMutator[] mutators; + + ProductMutator(SerializingMutator[] mutators) { + requireNonNullElements(mutators); + require(mutators.length > 0, "mutators must not be empty"); + this.mutators = Arrays.copyOf(mutators, mutators.length); + } + + @Override + public Object[] read(DataInputStream in) throws IOException { + Object[] value = new Object[mutators.length]; + for (int i = 0; i < mutators.length; i++) { + value[i] = mutators[i].read(in); + } + return value; + } + + @Override + public Object[] readExclusive(InputStream in) throws IOException { + Object[] value = new Object[mutators.length]; + int lastIndex = mutators.length - 1; + DataInputStream endlessData = new DataInputStream(extendWithZeros(in)); + for (int i = 0; i < lastIndex; i++) { + value[i] = mutators[i].read(endlessData); + } + value[lastIndex] = mutators[lastIndex].readExclusive(in); + return value; + } + + @Override + public void write(Object[] value, DataOutputStream out) throws IOException { + for (int i = 0; i < mutators.length; i++) { + mutators[i].write(value[i], out); + } + } + + @Override + public void writeExclusive(Object[] value, OutputStream out) throws IOException { + DataOutputStream dataOut = new DataOutputStream(out); + int lastIndex = mutators.length - 1; + for (int i = 0; i < lastIndex; i++) { + mutators[i].write(value[i], dataOut); + } + mutators[lastIndex].writeExclusive(value[lastIndex], out); + } + + @Override + protected Object[] makeDefaultInstance() { + return new Object[mutators.length]; + } + + @Override + public void initInPlace(Object[] reference, PseudoRandom prng) { + for (int i = 0; i < mutators.length; i++) { + reference[i] = mutators[i].init(prng); + } + } + + @Override + public void mutateInPlace(Object[] reference, PseudoRandom prng) { + int i = prng.indexIn(mutators); + reference[i] = mutators[i].mutate(reference[i], prng); + } + + @Override + public void crossOverInPlace(Object[] reference, Object[] otherReference, PseudoRandom prng) { + for (int i = 0; i < mutators.length; i++) { + SerializingMutator mutator = mutators[i]; + Object value = reference[i]; + Object otherValue = otherReference[i]; + Object crossedOver = prng.pickValue(value, otherValue, + () -> mutator.crossOver(value, otherValue, prng), INVERSE_PICK_VALUE_SUPPLIER_FREQUENCY); + if (crossedOver == otherReference) { + // If otherReference was picked, it needs to be detached as mutating + // it is prohibited in cross over. + crossedOver = mutator.detach(crossedOver); + } + reference[i] = crossedOver; + } + } + + @Override + public Object[] detach(Object[] value) { + Object[] clone = new Object[mutators.length]; + for (int i = 0; i < mutators.length; i++) { + clone[i] = mutators[i].detach(value[i]); + } + return clone; + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return stream(mutators) + .map(mutator -> mutator.toDebugString(isInCycle)) + .collect(joining(", ", "[", "]")); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/engine/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/engine/BUILD.bazel new file mode 100644 index 00000000..50bc180a --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/engine/BUILD.bazel @@ -0,0 +1,12 @@ +java_library( + name = "engine", + srcs = glob(["*.java"]), + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/mutation:__pkg__", + "//src/test/java/com/code_intelligence/jazzer/mutation:__subpackages__", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/api", + "//src/main/java/com/code_intelligence/jazzer/mutation/support", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/engine/SeededPseudoRandom.java b/src/main/java/com/code_intelligence/jazzer/mutation/engine/SeededPseudoRandom.java new file mode 100644 index 00000000..515f345b --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/engine/SeededPseudoRandom.java @@ -0,0 +1,288 @@ +/* + * 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.mutation.engine; + +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; + +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.support.Preconditions; +import com.code_intelligence.jazzer.mutation.support.RandomSupport; +import java.util.List; +import java.util.SplittableRandom; +import java.util.function.Supplier; + +public final class SeededPseudoRandom implements PseudoRandom { + // We use SplittableRandom instead of Random since it doesn't incur unnecessary synchronization + // overhead and uses a much better RNG under the hood that can generate all long values. + private final SplittableRandom random; + + public SeededPseudoRandom(long seed) { + this.random = new SplittableRandom(seed); + } + + @Override + public boolean choice() { + return random.nextBoolean(); + } + + @Override + public boolean trueInOneOutOf(int inverseFrequencyTrue) { + // Ensure that the outcome of the choice isn't fixed. + require(inverseFrequencyTrue >= 2); + return indexIn(inverseFrequencyTrue) == 0; + } + + @Override + public <T> T pickIn(T[] array) { + return array[indexIn(array.length)]; + } + + @Override + public <T> T pickIn(List<T> list) { + return list.get(indexIn(list.size())); + } + + @Override + public <T> int indexIn(T[] array) { + return indexIn(array.length); + } + + @Override + public <T> int indexIn(List<T> list) { + return indexIn(list.size()); + } + + @Override + public int indexIn(int range) { + require(range >= 1); + // TODO: Replace random.nextInt(length) with the fast version of + // https://lemire.me/blog/2016/06/30/fast-random-shuffling/, which avoids a modulo operation. + // It's slightly more biased for large bounds, but indices and choices tend to be small and + // are generated frequently (e.g. when picking a submutator). + return random.nextInt(range); + } + + @Override + public <T> int otherIndexIn(T[] array, int currentIndex) { + return otherIndexIn(array.length, currentIndex); + } + + @Override + public int otherIndexIn(int range, int currentIndex) { + int otherIndex = currentIndex + closedRange(1, range - 1); + if (otherIndex < range) { + return otherIndex; + } else { + return otherIndex - range; + } + } + + @Override + public int closedRange(int lowerInclusive, int upperInclusive) { + require(lowerInclusive <= upperInclusive); + int range = upperInclusive - lowerInclusive + 1; + if (range > 0) { + return lowerInclusive + random.nextInt(range); + } else { + // The interval [lowerInclusive, upperInclusive] covers at least half of the + // [Integer.MIN_VALUE, Integer.MAX_VALUE] range, fall back to rejection sampling with an + // expected number of samples <= 2. + int r; + do { + r = random.nextInt(); + } while (r < lowerInclusive); + return r; + } + } + + @Override + public long closedRange(long lowerInclusive, long upperInclusive) { + require(lowerInclusive <= upperInclusive); + if (upperInclusive < Long.MAX_VALUE) { + // upperInclusive + 1 <= Long.MAX_VALUE + return random.nextLong(lowerInclusive, upperInclusive + 1); + } else if (lowerInclusive > 0) { + // upperInclusive + 1 - lowerInclusive <= Long.MAX_VALUE + return lowerInclusive + random.nextLong(upperInclusive + 1 - lowerInclusive); + } else { + // The interval [lowerInclusive, Long.MAX_VALUE] covers at least half of the + // [Long.MIN_VALUE, Long.MAX_VALUE] range, fall back to rejection sampling with an expected + // number of samples <= 2. + long r; + do { + r = random.nextLong(); + } while (r < lowerInclusive); + return r; + } + } + + // This function always returns a finite value + @Override + public float closedRange(float lowerInclusive, float upperInclusive) { + require(lowerInclusive <= upperInclusive); + if (lowerInclusive == upperInclusive) { + require(Double.isFinite(lowerInclusive)); + return lowerInclusive; + } + // Special case: [Float.NEGATIVE_INFINITY, -Float.MAX_VALUE] + if (lowerInclusive == Float.NEGATIVE_INFINITY && upperInclusive == -Float.MAX_VALUE) + return -Float.MAX_VALUE; + // Special case: [Float.MAX_VALUE, Float.POSITIVE_INFINITY] + if (lowerInclusive == Float.MAX_VALUE && upperInclusive == Float.POSITIVE_INFINITY) + return Float.MAX_VALUE; + float limitedLower = + lowerInclusive == Float.NEGATIVE_INFINITY ? -Float.MAX_VALUE : lowerInclusive; + float limitedUpper = + upperInclusive == Float.POSITIVE_INFINITY ? Float.MAX_VALUE : upperInclusive; + + // nextDouble(start, bound) is exclusive of bound, so we use Math.nextUp to extend the bound to + // the next representable double. The maximal possible range of a float is always finite when + // represented as a double. Therefore, we can safely use nextDouble and convert it to a float. + return (float) random.nextDouble((double) limitedLower, Math.nextUp((double) limitedUpper)); + } + + // This function always returns a finite value + @Override + public double closedRange(double lowerInclusive, double upperInclusive) { + require(lowerInclusive <= upperInclusive); + if (lowerInclusive == upperInclusive) { + require(Double.isFinite(lowerInclusive)); + return lowerInclusive; + } + // Special case: [Double.NEGATIVE_INFINITY, -Double.MAX_VALUE] + if (lowerInclusive == Double.NEGATIVE_INFINITY && upperInclusive == -Double.MAX_VALUE) + return -Double.MAX_VALUE; + // Special case: [Double.MAX_VALUE, Double.POSITIVE_INFINITY) + if (lowerInclusive == Double.MAX_VALUE && upperInclusive == Double.POSITIVE_INFINITY) + return Double.MAX_VALUE; + + // nextDouble(start, bound) cannot deal with infinite values, so we need to limit them + double limitedLower = + lowerInclusive == Double.NEGATIVE_INFINITY ? -Double.MAX_VALUE : lowerInclusive; + double limitedUpper = + upperInclusive == Double.POSITIVE_INFINITY ? Double.MAX_VALUE : upperInclusive; + + // After limiting, the range may contain only a single value: return that + if (limitedLower == limitedUpper) + return limitedLower; + + // random.nextDouble() is exclusive of the upper bound. To include the upper bound, + // we extend the bound to the next double value by using Math.nextUp(limitedUpper). + double nextUpper = + (limitedUpper == Double.MAX_VALUE) ? limitedUpper : Math.nextUp(limitedUpper); + + // This, however, leads to a problem when the upper bound is Double.MAX_VALUE, because the next + // double after that is Double.POSITIVE_INFINITY. This case is treated the same as infinite + // range case, in the else branch. + boolean couldExtendRange = nextUpper != limitedUpper; + + // nextDouble(start, bound) can only deal with finite ranges + if (Double.isFinite(nextUpper - limitedLower) && couldExtendRange) { + double result = random.nextDouble(limitedLower, nextUpper); + // Clamp random.nextDouble() to the upper bound. + // This is a workaround for RandomSupport.nextDouble() that causes it to + // return values greater than upper bound. + // See https://bugs.openjdk.org/browse/JDK-8281183 for a list of affected JDK versions. + if (result > limitedUpper) + result = limitedUpper; + return result; + } else { + // Ranges that exceeds the maximum representable double value, or ranges that could not be + // extended scale a random n from range [0; 1] onto the range [limitLower, limitUpper] + // limitedLower * (1 - n) + limitedUpper * n - is the same as: + // limitedLower + (limitedUpper - limitedLower) * n + // limitedLower + range * n + double n = random.nextDouble(0.0, Math.nextUp(1.0)); + return limitedLower * (1 - n) + limitedUpper * n; + } + } + + @Override + public void bytes(byte[] bytes) { + RandomSupport.nextBytes(random, bytes); + } + + @Override + public int closedRangeBiasedTowardsSmall(int upperInclusive) { + if (upperInclusive == 0) { + return 0; + } + Preconditions.require(upperInclusive > 0); + // Modified from (Apache-2.0) + // https://github.com/abseil/abseil-cpp/blob/2927340217c37328319b5869285a6dcdbc13e7a7/absl/random/zipf_distribution.h + // by inlining the values v = 1 and q = 2. + final double kd = upperInclusive; + final double hxm = zipf_h(kd + 0.5); + final double h0x5 = -1.0 / 1.5; + final double elogv_q = 1.0; + final double hx0_minus_hxm = (h0x5 - elogv_q) - hxm; + final double s = 0.46153846153846123; + double k; + while (true) { + final double v = random.nextDouble(); + final double u = hxm + v * hx0_minus_hxm; + final double x = zipf_hinv(u); + k = Math.floor(x + 0.5); + if (k > kd) { + continue; + } + if (k - x <= s) { + break; + } + final double h = zipf_h(k + 0.5); + final double r = zipf_pow_negative_q(1.0 + k); + if (u >= h - r) { + break; + } + } + return (int) k; + } + + @Override + public int closedRangeBiasedTowardsSmall(int lowerInclusive, int upperInclusive) { + return lowerInclusive + closedRangeBiasedTowardsSmall(upperInclusive - lowerInclusive); + } + + private static double zipf_h(double x) { + return -1.0 / (x + 1.0); + } + + private static double zipf_hinv(double x) { + return -1.0 + -1.0 / x; + } + + private static double zipf_pow_negative_q(double x) { + return 1.0 / (x * x); + } + + @Override + public <T> T pickValue( + T value, T otherValue, Supplier<T> supplier, int inverseSupplierFrequency) { + if (trueInOneOutOf(inverseSupplierFrequency)) { + return supplier.get(); + } else if (choice()) { + return value; + } else { + return otherValue; + } + } + + @Override + public long nextLong() { + return random.nextLong(); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/BUILD.bazel new file mode 100644 index 00000000..b922a86a --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/BUILD.bazel @@ -0,0 +1,14 @@ +java_library( + name = "mutator", + srcs = glob(["*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/api", + "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", + "//src/main/java/com/code_intelligence/jazzer/mutation/api", + "//src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection", + "//src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang", + "//src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto", + "//src/main/java/com/code_intelligence/jazzer/mutation/support", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java new file mode 100644 index 00000000..fcc4d7ea --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java @@ -0,0 +1,81 @@ +/* + * 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.mutation.mutator; + +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.visitAnnotatedType; +import static java.lang.String.format; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; + +import com.code_intelligence.jazzer.mutation.annotation.AppliesTo; +import com.code_intelligence.jazzer.mutation.api.ChainedMutatorFactory; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.mutator.collection.CollectionMutators; +import com.code_intelligence.jazzer.mutation.mutator.lang.LangMutators; +import com.code_intelligence.jazzer.mutation.mutator.proto.ProtoMutators; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; + +public final class Mutators { + private Mutators() {} + + public static MutatorFactory newFactory() { + return new ChainedMutatorFactory( + LangMutators.newFactory(), CollectionMutators.newFactory(), ProtoMutators.newFactory()); + } + + /** + * Throws an exception if any annotation on {@code type} violates the restrictions of its + * {@link AppliesTo} meta-annotation. + */ + public static void validateAnnotationUsage(AnnotatedType type) { + visitAnnotatedType(type, (clazz, annotations) -> { + outer: + for (Annotation annotation : annotations) { + AppliesTo appliesTo = annotation.annotationType().getAnnotation(AppliesTo.class); + if (appliesTo == null) { + continue; + } + for (Class<?> allowedClass : appliesTo.value()) { + if (allowedClass == clazz) { + continue outer; + } + } + for (Class<?> allowedSuperClass : appliesTo.subClassesOf()) { + if (allowedSuperClass.isAssignableFrom(clazz)) { + continue outer; + } + } + + String helpText = ""; + if (appliesTo.value().length != 0) { + helpText = stream(appliesTo.value()).map(Class::getName).collect(joining(", ")); + } + if (appliesTo.subClassesOf().length != 0) { + if (!helpText.isEmpty()) { + helpText += "as well as "; + } + helpText += "subclasses of "; + helpText += stream(appliesTo.subClassesOf()).map(Class::getName).collect(joining(", ")); + } + // Use the simple name as our annotations live in a single package. + throw new IllegalArgumentException(format("%s does not apply to %s, only applies to %s", + annotation.annotationType().getSimpleName(), clazz.getName(), helpText)); + } + }); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/BUILD.bazel new file mode 100644 index 00000000..288b700a --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/BUILD.bazel @@ -0,0 +1,13 @@ +java_library( + name = "collection", + srcs = glob(["*.java"]), + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/mutator:__pkg__", + "//src/test/java/com/code_intelligence/jazzer/mutation/mutator:__subpackages__", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", + "//src/main/java/com/code_intelligence/jazzer/mutation/api", + "//src/main/java/com/code_intelligence/jazzer/mutation/support", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkCrossOvers.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkCrossOvers.java new file mode 100644 index 00000000..c124f517 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkCrossOvers.java @@ -0,0 +1,226 @@ +/* + * 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.mutation.mutator.collection; + +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +final class ChunkCrossOvers { + private ChunkCrossOvers() {} + + static <T> void insertChunk(List<T> list, List<T> otherList, int maxSize, PseudoRandom prng) { + int maxChunkSize = Math.min(maxSize - list.size(), Math.min(list.size(), otherList.size())); + withChunk(list, otherList, maxChunkSize, prng, + (fromPos, toPos, chunk) -> { list.addAll(toPos, chunk); }); + } + + static <T> void overwriteChunk(List<T> list, List<T> otherList, PseudoRandom prng) { + int maxChunkSize = Math.min(list.size(), otherList.size()); + withChunkElements(list, otherList, maxChunkSize, prng, list::set); + } + + static <T> void crossOverChunk( + List<T> list, List<T> otherList, SerializingMutator<T> elementMutator, PseudoRandom prng) { + int maxChunkSize = Math.min(list.size(), otherList.size()); + withChunkElements(list, otherList, maxChunkSize, prng, (toPos, element) -> { + list.set(toPos, elementMutator.crossOver(list.get(toPos), element, prng)); + }); + } + + @FunctionalInterface + private interface ChunkListOperation<T> { + void apply(int fromPos, int toPos, List<T> chunk); + } + + @FunctionalInterface + private interface ChunkListElementOperation<T> { + void apply(int toPos, T chunk); + } + + static private <T> void withChunk(List<T> list, List<T> otherList, int maxChunkSize, + PseudoRandom prng, ChunkListOperation<T> operation) { + if (maxChunkSize == 0) { + return; + } + int chunkSize = prng.closedRangeBiasedTowardsSmall(1, maxChunkSize); + int fromPos = prng.closedRange(0, otherList.size() - chunkSize); + int toPos = prng.closedRange(0, list.size() - chunkSize); + List<T> chunk = otherList.subList(fromPos, fromPos + chunkSize); + operation.apply(fromPos, toPos, chunk); + } + + static private <T> void withChunkElements(List<T> list, List<T> otherList, int maxChunkSize, + PseudoRandom prng, ChunkListElementOperation<T> operation) { + withChunk(list, otherList, maxChunkSize, prng, (fromPos, toPos, chunk) -> { + for (int i = 0; i < chunk.size(); i++) { + operation.apply(toPos + i, chunk.get(i)); + } + }); + } + + static <K, V> void insertChunk( + Map<K, V> map, Map<K, V> otherMap, int maxSize, PseudoRandom prng) { + int originalSize = map.size(); + int maxChunkSize = Math.min(maxSize - originalSize, otherMap.size()); + withChunk(map, otherMap, maxChunkSize, prng, (fromIterator, toIterator, chunkSize) -> { + // insertChunk only inserts new entries and does not overwrite existing + // ones. As skipping those entries would lead to fewer insertions than + // requested, loop over the rest of the map to fill the chunk. + while (map.size() < originalSize + chunkSize && fromIterator.hasNext()) { + Entry<K, V> entry = fromIterator.next(); + if (!map.containsKey(entry.getKey())) { + map.put(entry.getKey(), entry.getValue()); + } + } + }); + } + + static <K, V> void overwriteChunk(Map<K, V> map, Map<K, V> otherMap, PseudoRandom prng) { + int maxChunkSize = Math.min(map.size(), otherMap.size()); + withChunk(map, otherMap, maxChunkSize, prng, (fromIterator, toIterator, chunkSize) -> { + // As keys can not be overwritten, only removed and new ones added, this + // cross over overwrites the values. Removal of keys is handled by the + // removeChunk mutation. Value equality is not checked here. + for (int i = 0; i < chunkSize; i++) { + Entry<K, V> from = fromIterator.next(); + Entry<K, V> to = toIterator.next(); + to.setValue(from.getValue()); + } + }); + } + + static <K, V> void crossOverChunk(Map<K, V> map, Map<K, V> otherMap, + SerializingMutator<K> keyMutator, SerializingMutator<V> valueMutator, PseudoRandom prng) { + if (prng.choice()) { + crossOverChunkKeys(map, otherMap, keyMutator, prng); + } else { + crossOverChunkValues(map, otherMap, valueMutator, prng); + } + } + + private static <K, V> void crossOverChunkKeys( + Map<K, V> map, Map<K, V> otherMap, SerializingMutator<K> keyMutator, PseudoRandom prng) { + int maxChunkSize = Math.min(map.size(), otherMap.size()); + withChunk(map, otherMap, maxChunkSize, prng, (fromIterator, toIterator, chunkSize) -> { + Map<K, V> entriesToAdd = new LinkedHashMap<>(chunkSize); + for (int i = 0; i < chunkSize; i++) { + Entry<K, V> to = toIterator.next(); + Entry<K, V> from = fromIterator.next(); + + // The entry has to be removed from the map before the cross-over, as + // mutating its key could cause problems in subsequent lookups. + // Furthermore, no new entries may be added while using the iterator, + // so crossed-over keys are collected for later addition. + K key = to.getKey(); + V value = to.getValue(); + toIterator.remove(); + + // As cross-overs do not guarantee to mutate the given object, no + // checks if the crossed over key already exists in the map are + // performed. This potentially overwrites existing entries or + // generates equal keys. + // In case of cross over this behavior is acceptable. + K newKey = keyMutator.crossOver(key, from.getKey(), prng); + + // Prevent null keys, as those are not allowed in some map implementations. + if (newKey != null) { + entriesToAdd.put(newKey, value); + } + } + map.putAll(entriesToAdd); + }); + } + + private static <K, V> void crossOverChunkValues( + Map<K, V> map, Map<K, V> otherMap, SerializingMutator<V> valueMutator, PseudoRandom prng) { + int maxChunkSize = Math.min(map.size(), otherMap.size()); + withChunkElements(map, otherMap, maxChunkSize, prng, (fromEntry, toEntry) -> { + // As cross-overs do not guarantee to mutate the given object, no + // checks if a new value is produced are performed. + V newValue = valueMutator.crossOver(toEntry.getValue(), fromEntry.getValue(), prng); + + // The cross-over could have already mutated value, but explicitly set it + // through the iterator to be sure. + toEntry.setValue(newValue); + }); + } + + @FunctionalInterface + private interface ChunkMapOperation<K, V> { + void apply(Iterator<Entry<K, V>> fromIterator, Iterator<Entry<K, V>> toIterator, int chunkSize); + } + + @FunctionalInterface + private interface ChunkMapElementOperation<K, V> { + void apply(Entry<K, V> fromEntry, Entry<K, V> toEntry); + } + + static <K, V> void withChunk(Map<K, V> map, Map<K, V> otherMap, int maxChunkSize, + PseudoRandom prng, ChunkMapOperation<K, V> operation) { + int chunkSize = prng.closedRangeBiasedTowardsSmall(1, maxChunkSize); + int fromChunkOffset = prng.closedRange(0, otherMap.size() - chunkSize); + int toChunkOffset = prng.closedRange(0, map.size() - chunkSize); + Iterator<Entry<K, V>> fromIterator = otherMap.entrySet().iterator(); + for (int i = 0; i < fromChunkOffset; i++) { + fromIterator.next(); + } + Iterator<Entry<K, V>> toIterator = map.entrySet().iterator(); + for (int i = 0; i < toChunkOffset; i++) { + toIterator.next(); + } + operation.apply(fromIterator, toIterator, chunkSize); + } + + static <K, V> void withChunkElements(Map<K, V> map, Map<K, V> otherMap, int maxChunkSize, + PseudoRandom prng, ChunkMapElementOperation<K, V> operation) { + withChunk(map, otherMap, maxChunkSize, prng, (fromIterator, toIterator, chunkSize) -> { + for (int i = 0; i < chunkSize; i++) { + operation.apply(fromIterator.next(), toIterator.next()); + } + }); + } + + public enum CrossOverAction { + INSERT_CHUNK, + OVERWRITE_CHUNK, + CROSS_OVER_CHUNK, + NOOP; + + public static CrossOverAction pickRandomCrossOverAction( + Collection<?> reference, Collection<?> otherReference, int maxSize, PseudoRandom prng) { + List<CrossOverAction> actions = new ArrayList<>(); + if (reference.size() < maxSize && !otherReference.isEmpty()) { + actions.add(INSERT_CHUNK); + } + if (!reference.isEmpty() && !otherReference.isEmpty()) { + actions.add(OVERWRITE_CHUNK); + actions.add(CROSS_OVER_CHUNK); + } + if (actions.isEmpty()) { + return NOOP; // prevent NPE + } + return prng.pickIn(actions); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkMutations.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkMutations.java new file mode 100644 index 00000000..d5260289 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ChunkMutations.java @@ -0,0 +1,243 @@ +/* + * 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.mutation.mutator.collection; + +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.api.ValueMutator; +import com.code_intelligence.jazzer.mutation.support.Preconditions; +import java.util.AbstractList; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; + +// Based on (Apache-2.0) +// https://github.com/google/fuzztest/blob/f81257ed70ec7b9c191b633588cb6e39c42da5e4/fuzztest/internal/domains/container_mutation_helpers.h +@SuppressWarnings("unchecked") +final class ChunkMutations { + private static final int MAX_FAILED_INSERTION_ATTEMPTS = 100; + + private ChunkMutations() {} + + static <T> void deleteRandomChunk(List<T> list, int minSize, PseudoRandom prng) { + int oldSize = list.size(); + int minFinalSize = Math.max(minSize, oldSize / 2); + int chunkSize = prng.closedRangeBiasedTowardsSmall(1, oldSize - minFinalSize); + int chunkOffset = prng.closedRange(0, oldSize - chunkSize); + + list.subList(chunkOffset, chunkOffset + chunkSize).clear(); + } + + static <T> void deleteRandomChunk(Collection<T> collection, int minSize, PseudoRandom prng) { + int oldSize = collection.size(); + int minFinalSize = Math.max(minSize, oldSize / 2); + int chunkSize = prng.closedRangeBiasedTowardsSmall(1, oldSize - minFinalSize); + int chunkOffset = prng.closedRange(0, oldSize - chunkSize); + + Iterator<T> it = collection.iterator(); + for (int i = 0; i < chunkOffset; i++) { + it.next(); + } + for (int i = chunkOffset; i < chunkOffset + chunkSize; i++) { + it.next(); + it.remove(); + } + } + + static <T> void insertRandomChunk( + List<T> list, int maxSize, SerializingMutator<T> elementMutator, PseudoRandom prng) { + int oldSize = list.size(); + int chunkSize = prng.closedRangeBiasedTowardsSmall(1, maxSize - oldSize); + int chunkOffset = prng.closedRange(0, oldSize); + + T baseElement = elementMutator.init(prng); + T[] chunk = (T[]) new Object[chunkSize]; + for (int i = 0; i < chunk.length; i++) { + chunk[i] = elementMutator.detach(baseElement); + } + // ArrayList#addAll relies on Collection#toArray, but Arrays#asList returns a List whose + // toArray() always makes a copy. We avoid this by using a custom list implementation. + list.addAll(chunkOffset, new ArraySharingList<>(chunk)); + } + + static <T> boolean insertRandomChunk(Set<T> set, Consumer<T> addIfNew, int maxSize, + ValueMutator<T> elementMutator, PseudoRandom prng) { + int oldSize = set.size(); + int chunkSize = prng.closedRangeBiasedTowardsSmall(1, maxSize - oldSize); + return growBy(set, addIfNew, chunkSize, () -> elementMutator.init(prng)); + } + + static <T> void mutateRandomChunk(List<T> list, ValueMutator<T> mutator, PseudoRandom prng) { + int size = list.size(); + int chunkSize = prng.closedRangeBiasedTowardsSmall(1, size); + int chunkOffset = prng.closedRange(0, size - chunkSize); + + for (int i = chunkOffset; i < chunkOffset + chunkSize; i++) { + list.set(i, mutator.mutate(list.get(i), prng)); + } + } + + static <K, V, KW, VW> boolean mutateRandomKeysChunk( + Map<K, V> map, SerializingMutator<K> keyMutator, PseudoRandom prng) { + int originalSize = map.size(); + int chunkSize = prng.closedRangeBiasedTowardsSmall(1, originalSize); + int chunkOffset = prng.closedRange(0, originalSize - chunkSize); + + // To ensure that mutating keys actually results in the set of keys changing and not just their + // values (which is what #mutateRandomValuesChunk is for), we keep the keys to mutate in the + // map, try to add new keys (that are therefore distinct from the keys to mutate) and only + // remove the successfully mutated keys in the end. + ArrayDeque<KW> keysToMutate = new ArrayDeque<>(chunkSize); + ArrayDeque<VW> values = new ArrayDeque<>(chunkSize); + ArrayList<K> keysToRemove = new ArrayList<>(chunkSize); + Iterator<Map.Entry<K, V>> it = map.entrySet().iterator(); + for (int i = 0; i < chunkOffset; i++) { + it.next(); + } + for (int i = chunkOffset; i < chunkOffset + chunkSize; i++) { + Map.Entry<K, V> entry = it.next(); + // ArrayDeque cannot hold null elements, which requires us to replace null with a sentinel. + // Also detach the key as keys may be mutable and mutation could destroy them. + keysToMutate.add(boxNull(keyMutator.detach(entry.getKey()))); + values.add(boxNull(entry.getValue())); + keysToRemove.add(entry.getKey()); + } + + Consumer<K> addIfNew = key -> { + int sizeBeforeAdd = map.size(); + map.putIfAbsent(key, unboxNull(values.peekFirst())); + // The mutated key was new, try to mutate and add the next in line. + if (map.size() > sizeBeforeAdd) { + keysToMutate.removeFirst(); + values.removeFirst(); + } + }; + Supplier<K> nextCandidate = () -> { + // Mutate the next candidate in the queue. + K candidate = keyMutator.mutate(unboxNull(keysToMutate.removeFirst()), prng); + keysToMutate.addFirst(boxNull(candidate)); + return candidate; + }; + + growBy(map.keySet(), addIfNew, chunkSize, nextCandidate); + // Remove the original keys that were successfully mutated into new keys. Since the original + // keys have been kept in the map up to this point, all keys added were successfully mutated to + // be unequal to the original keys. + int grownBy = map.size() - originalSize; + keysToRemove.stream().limit(grownBy).forEach(map::remove); + return grownBy > 0; + } + + public static <K, V> void mutateRandomValuesChunk( + Map<K, V> map, ValueMutator<V> valueMutator, PseudoRandom prng) { + Collection<Map.Entry<K, V>> collection = map.entrySet(); + int oldSize = collection.size(); + int chunkSize = prng.closedRangeBiasedTowardsSmall(1, oldSize); + int chunkOffset = prng.closedRange(0, oldSize - chunkSize); + + Iterator<Map.Entry<K, V>> it = collection.iterator(); + for (int i = 0; i < chunkOffset; i++) { + it.next(); + } + for (int i = chunkOffset; i < chunkOffset + chunkSize; i++) { + Entry<K, V> entry = it.next(); + entry.setValue(valueMutator.mutate(entry.getValue(), prng)); + } + } + + static <T> boolean growBy( + Set<T> set, Consumer<T> addIfNew, int delta, Supplier<T> candidateSupplier) { + int oldSize = set.size(); + Preconditions.require(delta >= 0); + + final int targetSize = oldSize + delta; + int remainingAttempts = MAX_FAILED_INSERTION_ATTEMPTS; + int currentSize = set.size(); + while (currentSize < targetSize) { + // If addIfNew fails, the size of set will not increase. + addIfNew.accept(candidateSupplier.get()); + int newSize = set.size(); + if (newSize == currentSize && remainingAttempts-- == 0) { + return false; + } else { + currentSize = newSize; + } + } + return true; + } + + private static final Object BOXED_NULL = new Object(); + + private static <T, TW> TW boxNull(T object) { + return object != null ? (TW) object : (TW) BOXED_NULL; + } + + private static <T, TW> T unboxNull(TW object) { + return object != BOXED_NULL ? (T) object : null; + } + + public enum MutationAction { + DELETE_CHUNK, + INSERT_CHUNK, + MUTATE_CHUNK; + + public static MutationAction pickRandomMutationAction( + Collection<?> c, int minSize, int maxSize, PseudoRandom prng) { + List<MutationAction> actions = new ArrayList<>(); + if (c.size() > minSize) { + actions.add(DELETE_CHUNK); + } + if (c.size() < maxSize) { + actions.add(INSERT_CHUNK); + } + if (!c.isEmpty()) { + actions.add(MUTATE_CHUNK); + } + return prng.pickIn(actions); + } + } + + private static final class ArraySharingList<T> extends AbstractList<T> { + private final T[] array; + + ArraySharingList(T[] array) { + this.array = array; + } + + @Override + public T get(int i) { + return array[i]; + } + + @Override + public int size() { + return array.length; + } + + @Override + public Object[] toArray() { + return array; + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/CollectionMutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/CollectionMutators.java new file mode 100644 index 00000000..cf819f11 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/CollectionMutators.java @@ -0,0 +1,28 @@ +/* + * 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.mutation.mutator.collection; + +import com.code_intelligence.jazzer.mutation.api.ChainedMutatorFactory; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; + +public final class CollectionMutators { + private CollectionMutators() {} + + public static MutatorFactory newFactory() { + return new ChainedMutatorFactory(new ListMutatorFactory(), new MapMutatorFactory()); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ListMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ListMutatorFactory.java new file mode 100644 index 00000000..86ecbd4f --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ListMutatorFactory.java @@ -0,0 +1,167 @@ +/* + * 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.mutation.mutator.collection; + +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.CrossOverAction.pickRandomCrossOverAction; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.crossOverChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.insertChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.overwriteChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.MutationAction.pickRandomMutationAction; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.deleteRandomChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.insertRandomChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.mutateRandomChunk; +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.parameterTypeIfParameterized; +import static java.lang.Math.min; +import static java.lang.String.format; + +import com.code_intelligence.jazzer.mutation.annotation.WithSize; +import com.code_intelligence.jazzer.mutation.api.Debuggable; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.support.RandomSupport; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.reflect.AnnotatedType; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +final class ListMutatorFactory extends MutatorFactory { + @Override + public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) { + Optional<WithSize> withSize = Optional.ofNullable(type.getAnnotation(WithSize.class)); + int minSize = withSize.map(WithSize::min).orElse(ListMutator.DEFAULT_MIN_SIZE); + int maxSize = withSize.map(WithSize::max).orElse(ListMutator.DEFAULT_MAX_SIZE); + return parameterTypeIfParameterized(type, List.class) + .flatMap(factory::tryCreate) + .map(elementMutator -> new ListMutator<>(elementMutator, minSize, maxSize)); + } + + private static final class ListMutator<T> extends SerializingInPlaceMutator<List<T>> { + private static final int DEFAULT_MIN_SIZE = 0; + private static final int DEFAULT_MAX_SIZE = 1000; + + private final SerializingMutator<T> elementMutator; + private final int minSize; + private final int maxSize; + + ListMutator(SerializingMutator<T> elementMutator, int minSize, int maxSize) { + this.elementMutator = elementMutator; + this.minSize = minSize; + this.maxSize = maxSize; + require(maxSize >= 1, format("WithSize#max=%d needs to be greater than 0", maxSize)); + require(minSize >= 0, format("WithSize#min=%d needs to be positive", minSize)); + require(minSize <= maxSize, + format("WithSize#min=%d needs to be smaller or equal than WithSize#max=%d", minSize, + maxSize)); + } + + @Override + public List<T> read(DataInputStream in) throws IOException { + int size = RandomSupport.clamp(in.readInt(), minSize, maxSize); + ArrayList<T> list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.add(elementMutator.read(in)); + } + return list; + } + + @Override + public void write(List<T> list, DataOutputStream out) throws IOException { + out.writeInt(list.size()); + for (T element : list) { + elementMutator.write(element, out); + } + } + + @Override + protected List<T> makeDefaultInstance() { + return new ArrayList<>(maxInitialSize()); + } + + @Override + public void initInPlace(List<T> list, PseudoRandom prng) { + int targetSize = prng.closedRange(minInitialSize(), maxInitialSize()); + list.clear(); + for (int i = 0; i < targetSize; i++) { + list.add(elementMutator.init(prng)); + } + } + + @Override + public void mutateInPlace(List<T> list, PseudoRandom prng) { + switch (pickRandomMutationAction(list, minSize, maxSize, prng)) { + case DELETE_CHUNK: + deleteRandomChunk(list, minSize, prng); + break; + case INSERT_CHUNK: + insertRandomChunk(list, maxSize, elementMutator, prng); + break; + case MUTATE_CHUNK: + mutateRandomChunk(list, elementMutator, prng); + break; + default: + throw new IllegalStateException("unsupported action"); + } + } + + @Override + public void crossOverInPlace(List<T> reference, List<T> otherReference, PseudoRandom prng) { + // These cross-over functions don't remove entries, that is handled by + // the appropriate mutations on the result. + switch (pickRandomCrossOverAction(reference, otherReference, maxSize, prng)) { + case INSERT_CHUNK: + insertChunk(reference, otherReference, maxSize, prng); + break; + case OVERWRITE_CHUNK: + overwriteChunk(reference, otherReference, prng); + break; + case CROSS_OVER_CHUNK: + crossOverChunk(reference, otherReference, elementMutator, prng); + break; + default: + // Both lists are empty or could otherwise not be crossed over. + } + } + + @Override + public List<T> detach(List<T> value) { + return value.stream() + .map(elementMutator::detach) + .collect(Collectors.toCollection(() -> new ArrayList<>(value.size()))); + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return "List<" + elementMutator.toDebugString(isInCycle) + ">"; + } + + private int minInitialSize() { + return minSize; + } + + private int maxInitialSize() { + return min(maxSize, minSize + 1); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/MapMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/MapMutatorFactory.java new file mode 100644 index 00000000..fca8b5cb --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/MapMutatorFactory.java @@ -0,0 +1,205 @@ +/* + * 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.mutation.mutator.collection; + +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.CrossOverAction.pickRandomCrossOverAction; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.crossOverChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.insertChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkCrossOvers.overwriteChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.MutationAction.pickRandomMutationAction; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.deleteRandomChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.growBy; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.insertRandomChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.mutateRandomKeysChunk; +import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.mutateRandomValuesChunk; +import static com.code_intelligence.jazzer.mutation.support.Preconditions.check; +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.parameterTypesIfParameterized; +import static java.lang.Math.min; +import static java.lang.String.format; +import static java.util.stream.Collectors.toMap; + +import com.code_intelligence.jazzer.mutation.annotation.WithSize; +import com.code_intelligence.jazzer.mutation.api.Debuggable; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.support.RandomSupport; +import com.code_intelligence.jazzer.mutation.support.StreamSupport; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +final class MapMutatorFactory extends MutatorFactory { + @Override + public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) { + return parameterTypesIfParameterized(type, Map.class) + .map(parameterTypes + -> parameterTypes.stream() + .map(factory::tryCreate) + .flatMap(StreamSupport::getOrEmpty) + .collect(Collectors.toList())) + .map(elementMutators -> { + check(elementMutators.size() == 2); + int min = MapMutator.DEFAULT_MIN_SIZE; + int max = MapMutator.DEFAULT_MAX_SIZE; + for (Annotation annotation : type.getDeclaredAnnotations()) { + if (annotation instanceof WithSize) { + WithSize withSize = (WithSize) annotation; + min = withSize.min(); + max = withSize.max(); + } + } + return new MapMutator<>(elementMutators.get(0), elementMutators.get(1), min, max); + }); + } + + private static final class MapMutator<K, V> extends SerializingInPlaceMutator<Map<K, V>> { + private static final int DEFAULT_MIN_SIZE = 0; + private static final int DEFAULT_MAX_SIZE = 1000; + + private final SerializingMutator<K> keyMutator; + private final SerializingMutator<V> valueMutator; + private final int minSize; + private final int maxSize; + + MapMutator(SerializingMutator<K> keyMutator, SerializingMutator<V> valueMutator, int minSize, + int maxSize) { + this.keyMutator = keyMutator; + this.valueMutator = valueMutator; + this.minSize = Math.max(minSize, DEFAULT_MIN_SIZE); + this.maxSize = Math.min(maxSize, DEFAULT_MAX_SIZE); + + require(maxSize >= 1, format("WithSize#max=%d needs to be greater than 0", maxSize)); + // TODO: Add support for min > 0 to map. If min > 0, then #read can fail to construct + // sufficiently many distinct keys, but the mutation framework currently doesn't offer + // a way to handle this situation gracefully. It is also not clear what behavior users + // could reasonably expect in this situation in both regression test and fuzzing mode. + require(minSize == 0, "@WithSize#min != 0 is not yet supported for Map"); + } + + @Override + public Map<K, V> read(DataInputStream in) throws IOException { + int size = RandomSupport.clamp(in.readInt(), minSize, maxSize); + Map<K, V> map = new LinkedHashMap<>(size); + for (int i = 0; i < size; i++) { + map.put(keyMutator.read(in), valueMutator.read(in)); + } + // map may have less than size entries due to the potential for duplicates, but this is fine + // as we currently assert that minSize == 0. + return map; + } + + @Override + public void write(Map<K, V> map, DataOutputStream out) throws IOException { + out.writeInt(map.size()); + for (Map.Entry<K, V> entry : map.entrySet()) { + keyMutator.write(entry.getKey(), out); + valueMutator.write(entry.getValue(), out); + } + } + + @Override + protected Map<K, V> makeDefaultInstance() { + // Use a LinkedHashMap to ensure deterministic iteration order, which makes chunk-based + // mutations deterministic. The additional overhead compared to HashMap should be minimal. + return new LinkedHashMap<>(maxInitialSize()); + } + + @Override + public void initInPlace(Map<K, V> map, PseudoRandom prng) { + int targetSize = prng.closedRange(minInitialSize(), maxInitialSize()); + map.clear(); + growBy(map.keySet(), + key + -> map.putIfAbsent(key, valueMutator.init(prng)), + targetSize, () -> keyMutator.init(prng)); + if (map.size() < minSize) { + throw new IllegalStateException(String.format( + "Failed to create %d distinct elements of type %s to satisfy the @WithSize#minSize constraint on Map", + minSize, keyMutator)); + } + } + + @Override + public void mutateInPlace(Map<K, V> map, PseudoRandom prng) { + switch (pickRandomMutationAction(map.keySet(), minSize, maxSize, prng)) { + case DELETE_CHUNK: + deleteRandomChunk(map.keySet(), minSize, prng); + break; + case INSERT_CHUNK: + insertRandomChunk(map.keySet(), + key -> map.putIfAbsent(key, valueMutator.init(prng)), maxSize, keyMutator, prng); + break; + case MUTATE_CHUNK: + if (prng.choice() || !mutateRandomKeysChunk(map, keyMutator, prng)) { + mutateRandomValuesChunk(map, valueMutator, prng); + } + break; + default: + throw new IllegalStateException("unsupported action"); + } + } + + @Override + public void crossOverInPlace(Map<K, V> reference, Map<K, V> otherReference, PseudoRandom prng) { + switch ( + pickRandomCrossOverAction(reference.keySet(), otherReference.keySet(), maxSize, prng)) { + case INSERT_CHUNK: + insertChunk(reference, otherReference, maxSize, prng); + break; + case OVERWRITE_CHUNK: + overwriteChunk(reference, otherReference, prng); + break; + case CROSS_OVER_CHUNK: + crossOverChunk(reference, otherReference, keyMutator, valueMutator, prng); + break; + default: + // Both maps are empty or could otherwise not be crossed over. + } + } + + @Override + public Map<K, V> detach(Map<K, V> value) { + return value.entrySet().stream().collect(toMap(entry + -> keyMutator.detach(entry.getKey()), + entry -> valueMutator.detach(entry.getValue()))); + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return "Map<" + keyMutator.toDebugString(isInCycle) + "," + + valueMutator.toDebugString(isInCycle) + ">"; + } + + private int minInitialSize() { + return minSize; + } + + private int maxInitialSize() { + return min(maxSize, minSize + 1); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/BUILD.bazel new file mode 100644 index 00000000..5b234cee --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/BUILD.bazel @@ -0,0 +1,16 @@ +java_library( + name = "lang", + srcs = glob(["*.java"]), + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/mutator:__pkg__", + "//src/test/java/com/code_intelligence/jazzer/mutation/mutator:__subpackages__", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", + "//src/main/java/com/code_intelligence/jazzer/mutation/api", + "//src/main/java/com/code_intelligence/jazzer/mutation/combinator", + "//src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer", + "//src/main/java/com/code_intelligence/jazzer/mutation/support", + "@com_google_errorprone_error_prone_annotations//jar", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/BooleanMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/BooleanMutatorFactory.java new file mode 100644 index 00000000..a7dda971 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/BooleanMutatorFactory.java @@ -0,0 +1,79 @@ +/* + * 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.mutation.mutator.lang; + +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.findFirstParentIfClass; + +import com.code_intelligence.jazzer.mutation.api.Debuggable; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.google.errorprone.annotations.Immutable; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.reflect.AnnotatedType; +import java.util.Optional; +import java.util.function.Predicate; + +final class BooleanMutatorFactory extends MutatorFactory { + @Override + public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) { + return findFirstParentIfClass(type, boolean.class, Boolean.class) + .map(parent -> BooleanMutator.INSTANCE); + } + + @Immutable + private static final class BooleanMutator extends SerializingMutator<Boolean> { + private static final BooleanMutator INSTANCE = new BooleanMutator(); + + @Override + public Boolean read(DataInputStream in) throws IOException { + return in.readBoolean(); + } + + @Override + public void write(Boolean value, DataOutputStream out) throws IOException { + out.writeBoolean(value); + } + + @Override + public Boolean init(PseudoRandom prng) { + return prng.choice(); + } + + @Override + public Boolean mutate(Boolean value, PseudoRandom prng) { + return !value; + } + + @Override + public Boolean crossOver(Boolean value, Boolean otherValue, PseudoRandom prng) { + return prng.choice() ? value : otherValue; + } + + @Override + public String toDebugString(Predicate<Debuggable> isInLoop) { + return "Boolean"; + } + + @Override + public Boolean detach(Boolean value) { + return value; + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ByteArrayMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ByteArrayMutatorFactory.java new file mode 100644 index 00000000..cdd0d881 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ByteArrayMutatorFactory.java @@ -0,0 +1,227 @@ +/* + * 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.mutation.mutator.lang; + +import static com.code_intelligence.jazzer.mutation.support.InputStreamSupport.readAllBytes; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.findFirstParentIfClass; + +import com.code_intelligence.jazzer.mutation.annotation.WithLength; +import com.code_intelligence.jazzer.mutation.api.Debuggable; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.mutator.libfuzzer.LibFuzzerMutator; +import com.code_intelligence.jazzer.mutation.support.RandomSupport; +import com.google.errorprone.annotations.Immutable; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.AnnotatedType; +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Predicate; + +final class ByteArrayMutatorFactory extends MutatorFactory { + @Override + public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) { + Optional<WithLength> withLength = Optional.ofNullable(type.getAnnotation(WithLength.class)); + int minLength = withLength.map(WithLength::min).orElse(ByteArrayMutator.DEFAULT_MIN_LENGTH); + int maxLength = withLength.map(WithLength::max).orElse(ByteArrayMutator.DEFAULT_MAX_LENGTH); + + return findFirstParentIfClass(type, byte[].class) + .map(parent -> new ByteArrayMutator(minLength, maxLength)); + } + + @Immutable + private static final class ByteArrayMutator extends SerializingMutator<byte[]> { + private static final int DEFAULT_MIN_LENGTH = 0; + private static final int DEFAULT_MAX_LENGTH = 1000; + + private final int minLength; + + private final int maxLength; + + private ByteArrayMutator(int min, int max) { + this.minLength = min; + this.maxLength = max; + } + + @Override + public byte[] read(DataInputStream in) throws IOException { + int length = RandomSupport.clamp(in.readInt(), minLength, maxLength); + byte[] bytes = new byte[length]; + in.readFully(bytes); + return bytes; + } + + @Override + public byte[] readExclusive(InputStream in) throws IOException { + return readAllBytes(in); + } + + @Override + public void write(byte[] value, DataOutputStream out) throws IOException { + out.writeInt(value.length); + out.write(value); + } + + @Override + public void writeExclusive(byte[] value, OutputStream out) throws IOException { + out.write(value); + } + + @Override + public byte[] detach(byte[] value) { + return Arrays.copyOf(value, value.length); + } + + @Override + public byte[] init(PseudoRandom prng) { + int len = prng.closedRange(minInitialSize(), maxInitialSize()); + byte[] bytes = new byte[len]; + prng.bytes(bytes); + return bytes; + } + + private int minInitialSize() { + return minLength; + } + + private int maxInitialSize() { + // Allow some variation in length, but keep the initial elements well within reach of each + // other via a single mutation based on a Table of Recent Compares (ToRC) entry, which is + // currently limited to 64 bytes. + // Compared to List<T>, byte arrays can't result in recursive type hierarchies and thus don't + // to limit their expected initial size to be <= 1. + return Math.min(minLength + 16, maxLength); + } + + @Override + public byte[] mutate(byte[] value, PseudoRandom prng) { + int maxLengthIncrease = maxLength - value.length; + byte[] mutated = LibFuzzerMutator.mutateDefault(value, maxLengthIncrease); + return enforceLength(mutated); + } + + private byte[] enforceLength(byte[] mutated) { + // if the mutated array libfuzzer returns is too long or short, we truncate or extend it + // respectively. if we extend it, then copyOf will fill leftover bytes with 0 + if (mutated.length > maxLength) { + return Arrays.copyOf(mutated, maxLength); + } else if (mutated.length < minLength) { + return Arrays.copyOf(mutated, minLength); + } else { + return mutated; + } + } + + @Override + public byte[] crossOver(byte[] value, byte[] otherValue, PseudoRandom prng) { + // Passed in values are expected to already honor the min/max length constraints. + // As there does not seem to be an easy way to call libFuzzer's internal cross over + // algorithm, it is re-implemented in native Java. The algorithm is based on: + // https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/fuzzer/FuzzerMutate.cpp#L440 + // https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/fuzzer/FuzzerCrossOver.cpp#L19 + // + + if (value.length == 0 || otherValue.length == 0) { + return value; + } + + // TODO: Measure if this is fast enough. + byte[] out = null; + while (out == null) { + switch (prng.indexIn(3)) { + case 0: + out = intersect(value, otherValue, prng); + break; + case 1: + out = insertPart(value, otherValue, prng); + break; + case 2: + out = overwritePart(value, otherValue, prng); + break; + default: + throw new AssertionError("Invalid cross over function."); + } + } + return enforceLength(out); + } + + private static byte[] intersect(byte[] value, byte[] otherValue, PseudoRandom prng) { + int maxOutSize = prng.closedRange(0, Math.min(value.length, otherValue.length)); + byte[] out = new byte[maxOutSize]; + int outPos = 0; + int valuePos = 0; + int otherValuePos = 0; + boolean usingFirstValue = true; + while (outPos < out.length) { + if (usingFirstValue && valuePos < value.length) { + int extraSize = rndArraycopy(value, valuePos, out, outPos, prng); + outPos += extraSize; + valuePos += extraSize; + } else if (!usingFirstValue && otherValuePos < otherValue.length) { + int extraSize = rndArraycopy(otherValue, otherValuePos, out, outPos, prng); + outPos += extraSize; + otherValuePos += extraSize; + } + usingFirstValue = !usingFirstValue; + } + return out; + } + + private static int rndArraycopy( + byte[] val, int valPos, byte[] out, int outPos, PseudoRandom prng) { + int outSizeLeft = out.length - outPos; + int inSizeLeft = val.length - valPos; + int maxExtraSize = Math.min(outSizeLeft, inSizeLeft); + int extraSize = prng.closedRange(0, maxExtraSize); + System.arraycopy(val, valPos, out, outPos, extraSize); + return extraSize; + } + + private static byte[] insertPart(byte[] value, byte[] otherValue, PseudoRandom prng) { + int copySize = prng.closedRange(1, otherValue.length); + int f = otherValue.length - copySize; + int fromPos = f == 0 ? 0 : prng.indexIn(f); + int toPos = prng.indexIn(value.length); + int tailSize = value.length - toPos; + + byte[] out = new byte[value.length + copySize]; + System.arraycopy(value, 0, out, 0, toPos); + System.arraycopy(otherValue, fromPos, out, toPos, copySize); + System.arraycopy(value, toPos, out, toPos + copySize, tailSize); + return out; + } + + private static byte[] overwritePart(byte[] value, byte[] otherValue, PseudoRandom prng) { + int toPos = prng.indexIn(value.length); + int copySize = Math.min(prng.closedRange(1, value.length - toPos), otherValue.length); + int f = otherValue.length - copySize; + int fromPos = f == 0 ? 0 : prng.indexIn(f); + System.arraycopy(otherValue, fromPos, value, toPos, copySize); + return value; + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return "byte[]"; + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/EnumMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/EnumMutatorFactory.java new file mode 100644 index 00000000..80b9e6c9 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/EnumMutatorFactory.java @@ -0,0 +1,47 @@ +/* + * 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.mutation.mutator.lang; + +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateIndices; +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMap; +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable; +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asSubclassOrEmpty; + +import com.code_intelligence.jazzer.mutation.api.Debuggable; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import java.lang.reflect.AnnotatedType; +import java.util.Optional; +import java.util.function.Predicate; + +final class EnumMutatorFactory extends MutatorFactory { + @Override + public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) { + return asSubclassOrEmpty(type, Enum.class).map(parent -> { + require(((Class<Enum<?>>) type.getType()).getEnumConstants().length > 1, + String.format( + "%s defines less than two enum constants and can't be mutated. Use a constant instead.", + parent)); + Enum<?>[] values = ((Class<Enum<?>>) type.getType()).getEnumConstants(); + return mutateThenMap(mutateIndices(values.length), + (index) + -> values[index], + Enum::ordinal, (Predicate<Debuggable> inCycle) -> "Enum<" + parent.getSimpleName() + ">"); + }); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/FloatingPointMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/FloatingPointMutatorFactory.java new file mode 100644 index 00000000..17890a9f --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/FloatingPointMutatorFactory.java @@ -0,0 +1,597 @@ +/* + * 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.mutation.mutator.lang; + +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static java.lang.String.format; + +import com.code_intelligence.jazzer.mutation.annotation.DoubleInRange; +import com.code_intelligence.jazzer.mutation.annotation.FloatInRange; +import com.code_intelligence.jazzer.mutation.api.Debuggable; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.mutator.libfuzzer.LibFuzzerMutator; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.DoubleFunction; +import java.util.function.Predicate; +import java.util.stream.DoubleStream; + +final class FloatingPointMutatorFactory extends MutatorFactory { + @SuppressWarnings("unchecked") + private static final DoubleFunction<Double>[] mathFunctions = + new DoubleFunction[] {Math::acos, Math::asin, Math::atan, Math::cbrt, Math::ceil, Math::cos, + Math::cosh, Math::exp, Math::expm1, Math::floor, Math::log, Math::log10, Math::log1p, + Math::rint, Math::sin, Math::sinh, Math::sqrt, Math::tan, Math::tanh, Math::toDegrees, + Math::toRadians, n -> n * 0.5, n -> n * 2.0, n -> n * 0.333333333333333, n -> n * 3.0}; + + @Override + public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) { + if (!(type.getType() instanceof Class)) { + return Optional.empty(); + } + Class<?> clazz = (Class<?>) type.getType(); + + if (clazz == float.class || clazz == Float.class) { + return Optional.of( + new FloatMutator(type, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, true)); + } else if (clazz == double.class || clazz == Double.class) { + return Optional.of( + new DoubleMutator(type, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, true)); + } else { + return Optional.empty(); + } + } + + static final class FloatMutator extends SerializingMutator<Float> { + private static final int EXPONENT_INITIAL_BIT = 23; + private static final int MANTISSA_MASK = 0x7fffff; + private static final int EXPONENT_MASK = 0xff; + private static final int MANTISSA_RANDOM_WALK_RANGE = 1000; + private static final int EXPONENT_RANDOM_WALK_RANGE = Float.MAX_EXPONENT; + private static final int INVERSE_FREQUENCY_SPECIAL_VALUE = 1000; + + // Visible for testing. + final float minValue; + final float maxValue; + final boolean allowNaN; + private final float[] specialValues; + + FloatMutator(AnnotatedType type, float defaultMinValueForType, float defaultMaxValueForType, + boolean defaultAllowNaN) { + float minValue = defaultMinValueForType; + float maxValue = defaultMaxValueForType; + boolean allowNaN = defaultAllowNaN; + // InRange is not repeatable, so the loop body will apply at most once. + for (Annotation annotation : type.getAnnotations()) { + if (annotation instanceof FloatInRange) { + FloatInRange floatInRange = (FloatInRange) annotation; + minValue = floatInRange.min(); + maxValue = floatInRange.max(); + allowNaN = floatInRange.allowNaN(); + } + } + + require(minValue <= maxValue, + format("[%f, %f] is not a valid interval: %s", minValue, maxValue, type)); + require(minValue != maxValue, + format( + "[%f, %f] can not be mutated, use a constant instead: %s", minValue, maxValue, type)); + this.minValue = minValue; + this.maxValue = maxValue; + this.allowNaN = allowNaN; + this.specialValues = collectSpecialValues(minValue, maxValue); + } + + private float[] collectSpecialValues(float minValue, float maxValue) { + // stream of floats + List<Double> specialValues = + DoubleStream + .of(Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, 0.0f, -0.0f, Float.NaN, + Float.MAX_VALUE, Float.MIN_VALUE, -Float.MAX_VALUE, -Float.MIN_VALUE, + this.minValue, this.maxValue) + .filter(n -> (n >= minValue && n <= maxValue) || allowNaN && Double.isNaN(n)) + .distinct() + .sorted() + .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); + + float[] specialValuesArray = new float[specialValues.size()]; + for (int i = 0; i < specialValues.size(); i++) { + specialValuesArray[i] = (float) (double) specialValues.get(i); + } + return specialValuesArray; + } + + public float mutateWithLibFuzzer(float value) { + return LibFuzzerMutator.mutateDefault(value, this, 0); + } + + @Override + public Float init(PseudoRandom prng) { + if (prng.choice()) { + return specialValues[prng.closedRange(0, specialValues.length - 1)]; + } else { + return prng.closedRange(minValue, maxValue); + } + } + + @Override + public Float mutate(Float value, PseudoRandom prng) { + float result; + // small chance to return a special value + if (prng.trueInOneOutOf(INVERSE_FREQUENCY_SPECIAL_VALUE)) { + result = specialValues[prng.closedRange(0, specialValues.length - 1)]; + } else { + switch (prng.closedRange(0, 5)) { + case 0: + result = mutateWithBitFlip(value, prng); + break; + case 1: + result = mutateExponent(value, prng); + break; + case 2: + result = mutateMantissa(value, prng); + break; + case 3: + result = mutateWithMathematicalFn(value, prng); + break; + case 4: + result = mutateWithLibFuzzer(value); + break; + case 5: // random in range cannot exceed the given bounds (and cannot be NaN) + result = prng.closedRange(minValue, maxValue); + break; + default: + throw new IllegalStateException("Unknown mutation case"); + } + } + result = forceInRange(result, minValue, maxValue, allowNaN); + + // Repeating values are not allowed. + if (Float.compare(result, value) == 0) { + if (Float.isNaN(result)) { + return prng.closedRange(minValue, maxValue); + } else { // Change the value to the neighboring float. + if (result > minValue && result < maxValue) { + return prng.choice() ? Math.nextAfter(result, Float.NEGATIVE_INFINITY) + : Math.nextAfter(result, Float.POSITIVE_INFINITY); + } else if (result > minValue) { + return Math.nextAfter(result, Float.NEGATIVE_INFINITY); + } else + return Math.nextAfter(result, Float.POSITIVE_INFINITY); + } + } + + return result; + } + + static float forceInRange(float value, float minValue, float maxValue, boolean allowNaN) { + if ((value >= minValue && value <= maxValue) || (Float.isNaN(value) && allowNaN)) + return value; + + // Clamp infinite values + if (value == Float.POSITIVE_INFINITY) + return maxValue; + if (value == Float.NEGATIVE_INFINITY) + return minValue; + + // From here on limits should be finite + float finiteMax = Math.min(Float.MAX_VALUE, maxValue); + float finiteMin = Math.max(-Float.MAX_VALUE, minValue); + + // If NaN was allowed, it was handled above. Replace it by the midpoint of the range. + if (Float.isNaN(value)) + return finiteMin * 0.5f + finiteMax * 0.5f; + + float range = finiteMax - finiteMin; + if (range == 0f) + return finiteMin; + + float diff = value - finiteMin; + + if (Float.isFinite(diff) && Float.isFinite(range)) { + return finiteMin + Math.abs(diff % range); + } + + // diff, range, or both are infinite: divide both by 2, reduce, and multiply by 2. + float halfDiff = value * 0.5f - finiteMin * 0.5f; + + return finiteMin + (halfDiff % (finiteMax * 0.5f - finiteMin * 0.5f)) * 2.0f; + } + + public float mutateWithMathematicalFn(float value, PseudoRandom prng) { + double result = prng.pickIn(mathFunctions).apply(value); + return (float) result; + } + + private float mutateWithBitFlip(float value, PseudoRandom prng) { + int bits = Float.floatToRawIntBits(value); + int bitToFlip = prng.closedRange(0, 31); + bits ^= 1L << bitToFlip; + return Float.intBitsToFloat(bits); + } + + private float mutateExponent(float value, PseudoRandom prng) { + int bits = Float.floatToRawIntBits(value); + int exponent = ((bits >> EXPONENT_INITIAL_BIT) & EXPONENT_MASK) + + prng.closedRange(0, EXPONENT_RANDOM_WALK_RANGE); + bits = (bits & ~(EXPONENT_MASK << EXPONENT_INITIAL_BIT)) + | ((exponent % EXPONENT_MASK) << EXPONENT_INITIAL_BIT); + return Float.intBitsToFloat(bits); + } + + private float mutateMantissa(float value, PseudoRandom prng) { + int bits = Float.floatToRawIntBits(value); + + int mantissa = bits & MANTISSA_MASK; + switch (prng.closedRange(0, 2)) { + case 0: // + + mantissa = + (mantissa + prng.closedRange(-MANTISSA_RANDOM_WALK_RANGE, MANTISSA_RANDOM_WALK_RANGE)) + % MANTISSA_MASK; + break; + case 1: // * + mantissa = + (mantissa * prng.closedRange(-MANTISSA_RANDOM_WALK_RANGE, MANTISSA_RANDOM_WALK_RANGE)) + % MANTISSA_MASK; + break; + case 2: // / + int divisor = prng.closedRange(2, MANTISSA_RANDOM_WALK_RANGE); + if (prng.choice()) { + divisor = -divisor; + } + mantissa = (mantissa / divisor); + break; + default: + throw new IllegalStateException("Unknown mutation case for mantissa"); + } + bits = (bits & ~MANTISSA_MASK) | mantissa; + return Float.intBitsToFloat(bits); + } + + @Override + public Float crossOver(Float value, Float otherValue, PseudoRandom prng) { + float result; + switch (prng.closedRange(0, 2)) { + case 0: + result = crossOverMean(value, otherValue); + break; + case 1: + result = crossOverExponent(value, otherValue); + break; + case 2: + result = crossOverMantissa(value, otherValue); + break; + default: + throw new IllegalStateException("Unknown mutation case"); + } + return forceInRange(result, minValue, maxValue, allowNaN); + } + + private float crossOverMean(float value, float otherValue) { + return (float) ((((double) value) + ((double) otherValue)) / 2.0); + } + + private float crossOverExponent(float value, float otherValue) { + int bits = Float.floatToRawIntBits(value); + int otherExponent = + Float.floatToRawIntBits(otherValue) & (EXPONENT_MASK << EXPONENT_INITIAL_BIT); + int bitsWithOtherExponent = (bits & ~(EXPONENT_MASK << EXPONENT_INITIAL_BIT)) | otherExponent; + return Float.intBitsToFloat(bitsWithOtherExponent); + } + + private float crossOverMantissa(float value, float otherValue) { + int bits = Float.floatToRawIntBits(value); + int otherMantissa = Float.floatToRawIntBits(otherValue) & MANTISSA_MASK; + int bitsWithOtherMantissa = (bits & ~MANTISSA_MASK) | otherMantissa; + return Float.intBitsToFloat(bitsWithOtherMantissa); + } + + @Override + public Float read(DataInputStream in) throws IOException { + return forceInRange(in.readFloat(), minValue, maxValue, allowNaN); + } + + @Override + public void write(Float value, DataOutputStream out) throws IOException { + out.writeFloat(value); + } + + @Override + public Float detach(Float value) { + return value; + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return "Float"; + } + } + + static final class DoubleMutator extends SerializingMutator<Double> { + private static final long MANTISSA_RANDOM_WALK_RANGE = 1000; + private static final int EXPONENT_RANDOM_WALK_RANGE = Double.MAX_EXPONENT; + private static final int INVERSE_FREQUENCY_SPECIAL_VALUE = 1000; + private static final long MANTISSA_MASK = 0xfffffffffffffL; + private static final long EXPONENT_MASK = 0x7ffL; + private static final int EXPONENT_INITIAL_BIT = 52; + + // Visible for testing + final double minValue; + final double maxValue; + final boolean allowNaN; + private final double[] specialValues; + + DoubleMutator(AnnotatedType type, double defaultMinValueForType, double defaultMaxValueForType, + boolean defaultAllowNaN) { + double minValue = defaultMinValueForType; + double maxValue = defaultMaxValueForType; + boolean allowNaN = defaultAllowNaN; + // InRange is not repeatable, so the loop body will apply at most once. + for (Annotation annotation : type.getAnnotations()) { + if (annotation instanceof DoubleInRange) { + DoubleInRange doubleInRange = (DoubleInRange) annotation; + minValue = doubleInRange.min(); + maxValue = doubleInRange.max(); + allowNaN = doubleInRange.allowNaN(); + } + } + + require(!Double.isNaN(minValue) && !Double.isNaN(maxValue), + format("[%f, %f] is not a valid interval: %s", minValue, maxValue, type)); + require(minValue <= maxValue, + format("[%f, %f] is not a valid interval: %s", minValue, maxValue, type)); + require(minValue != maxValue, + format( + "[%f, %f] can not be mutated, use a constant instead: %s", minValue, maxValue, type)); + this.minValue = minValue; + this.maxValue = maxValue; + this.allowNaN = allowNaN; + this.specialValues = collectSpecialValues(minValue, maxValue); + } + + private double[] collectSpecialValues(double minValue, double maxValue) { + double[] specialValues = new double[] {Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, + 0.0, -0.0, Double.NaN, Double.MAX_VALUE, Double.MIN_VALUE, -Double.MAX_VALUE, + -Double.MIN_VALUE, this.minValue, this.maxValue}; + return Arrays.stream(specialValues) + .boxed() + .filter(value -> (allowNaN && value.isNaN()) || (value >= minValue && value <= maxValue)) + .distinct() + .sorted() + .mapToDouble(Double::doubleValue) + .toArray(); + } + + public double mutateWithLibFuzzer(double value) { + return LibFuzzerMutator.mutateDefault(value, this, 0); + } + + @Override + public Double init(PseudoRandom prng) { + if (prng.choice()) { + return specialValues[prng.closedRange(0, specialValues.length - 1)]; + } else { + return prng.closedRange(minValue, maxValue); + } + } + + @Override + public Double mutate(Double value, PseudoRandom prng) { + double result; + // small chance to return a special value + if (prng.trueInOneOutOf(INVERSE_FREQUENCY_SPECIAL_VALUE)) { + result = specialValues[prng.closedRange(0, specialValues.length - 1)]; + } else { + switch (prng.closedRange(0, 5)) { + case 0: + result = mutateWithBitFlip(value, prng); + break; + case 1: + result = mutateExponent(value, prng); + break; + case 2: + result = mutateMantissa(value, prng); + break; + case 3: + result = mutateWithMathematicalFn(value, prng); + break; + case 4: + result = mutateWithLibFuzzer(value); + break; + case 5: // random in range cannot exceed the given bounds (and cannot be NaN) + result = prng.closedRange(minValue, maxValue); + break; + default: + throw new IllegalStateException("Unknown mutation case"); + } + } + result = forceInRange(result, minValue, maxValue, allowNaN); + + // Repeating values are not allowed. + if (Double.compare(result, value) == 0) { + if (Double.isNaN(result)) { + return prng.closedRange(minValue, maxValue); + } else { // Change the value to the neighboring float. + if (result > minValue && result < maxValue) { + return prng.choice() ? Math.nextAfter(result, Double.NEGATIVE_INFINITY) + : Math.nextAfter(result, Double.POSITIVE_INFINITY); + } else if (result > minValue) { + return Math.nextAfter(result, Double.NEGATIVE_INFINITY); + } else + return Math.nextAfter(result, Double.POSITIVE_INFINITY); + } + } + + return result; + } + + static double forceInRange(double value, double minValue, double maxValue, boolean allowNaN) { + if ((value >= minValue && value <= maxValue) || (Double.isNaN(value) && allowNaN)) { + return value; + } + + // Clamp infinite values + if (value == Double.POSITIVE_INFINITY) + return maxValue; + if (value == Double.NEGATIVE_INFINITY) + return minValue; + + // From here on limits should be finite + double finiteMax = Math.min(Double.MAX_VALUE, maxValue); + double finiteMin = Math.max(-Double.MAX_VALUE, minValue); + + // If NaN was allowed, it was handled above. + // Here we replace NaN by the middle of the clamped finite range. + if (Double.isNaN(value)) { + // maxValue or minValue may be infinite, so we need to clamp them. + return minValue + + (Math.min(Double.MAX_VALUE, maxValue) * 0.5 + - Math.max(-Double.MAX_VALUE, minValue) * 0.5); + } + + double range = finiteMax - finiteMin; + if (range == 0) + return finiteMin; + + double diff = value - finiteMin; + + if (Double.isFinite(diff) && Double.isFinite(range)) { + return finiteMin + Math.abs(diff % range); + } + + // diff, range, or both are infinite: divide both by 2, reduce, and multiply by 2. + double halfDiff = value * 0.5 - finiteMin * 0.5; + return finiteMin + (halfDiff % (finiteMax * 0.5 - finiteMin * 0.5)) * 2.0; + } + + public double mutateWithMathematicalFn(double value, PseudoRandom prng) { + return prng.pickIn(mathFunctions).apply(value); + } + + public static double mutateWithBitFlip(double value, PseudoRandom prng) { + long bits = Double.doubleToRawLongBits(value); + int bitToFlip = prng.closedRange(0, 63); + bits ^= 1L << bitToFlip; + return Double.longBitsToDouble(bits); + } + + private static double mutateExponent(double value, PseudoRandom prng) { + long bits = Double.doubleToRawLongBits(value); + long exponent = ((bits >> EXPONENT_INITIAL_BIT) & EXPONENT_MASK) + + prng.closedRange(0, EXPONENT_RANDOM_WALK_RANGE); + bits = (bits & ~(EXPONENT_MASK << EXPONENT_INITIAL_BIT)) + | ((exponent % EXPONENT_MASK) << EXPONENT_INITIAL_BIT); + return Double.longBitsToDouble(bits); + } + + public static double mutateMantissa(double value, PseudoRandom prng) { + long bits = Double.doubleToRawLongBits(value); + long mantissa = bits & MANTISSA_MASK; + switch (prng.closedRange(0, 2)) { + case 0: // + + mantissa = + (mantissa + prng.closedRange(-MANTISSA_RANDOM_WALK_RANGE, MANTISSA_RANDOM_WALK_RANGE)) + % MANTISSA_MASK; + break; + case 1: // * + mantissa = + (mantissa * prng.closedRange(-MANTISSA_RANDOM_WALK_RANGE, MANTISSA_RANDOM_WALK_RANGE)) + % MANTISSA_MASK; + break; + case 2: // / + long divisor = prng.closedRange(2, MANTISSA_RANDOM_WALK_RANGE); + if (prng.choice()) { + divisor = -divisor; + } + mantissa = (mantissa / divisor); + break; + default: + throw new IllegalStateException("Unknown mutation case for mantissa"); + } + bits = (bits & ~MANTISSA_MASK) | mantissa; + return Double.longBitsToDouble(bits); + } + + @Override + public Double crossOver(Double value, Double otherValue, PseudoRandom prng) { + double result; + switch (prng.closedRange(0, 2)) { + case 0: + result = crossOverMean(value, otherValue); + break; + case 1: + result = crossOverExponent(value, otherValue); + break; + case 2: + result = crossOverMantissa(value, otherValue); + break; + default: + throw new IllegalStateException("Unknown mutation case"); + } + return forceInRange(result, minValue, maxValue, allowNaN); + } + + private double crossOverMean(double value, double otherValue) { + return (value * 0.5) + (otherValue * 0.5); + } + + private double crossOverExponent(double value, double otherValue) { + long bits = Double.doubleToRawLongBits(value); + long otherExponent = + Double.doubleToRawLongBits(otherValue) & (EXPONENT_MASK << EXPONENT_INITIAL_BIT); + long bitsWithOtherExponent = + (bits & ~(EXPONENT_MASK << EXPONENT_INITIAL_BIT)) | otherExponent; + return Double.longBitsToDouble(bitsWithOtherExponent); + } + + private double crossOverMantissa(double value, double otherValue) { + long bits = Double.doubleToRawLongBits(value); + long otherMantissa = Double.doubleToRawLongBits(otherValue) & MANTISSA_MASK; + long bitsWithOtherMantissa = (bits & ~MANTISSA_MASK) | otherMantissa; + return Double.longBitsToDouble(bitsWithOtherMantissa); + } + + @Override + public Double read(DataInputStream in) throws IOException { + return forceInRange(in.readDouble(), minValue, maxValue, allowNaN); + } + + @Override + public void write(Double value, DataOutputStream out) throws IOException { + out.writeDouble(value); + } + + @Override + public Double detach(Double value) { + return value; + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return "Double"; + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/IntegralMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/IntegralMutatorFactory.java new file mode 100644 index 00000000..e701e6ad --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/IntegralMutatorFactory.java @@ -0,0 +1,399 @@ +/* + * 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.mutation.mutator.lang; + +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static java.lang.String.format; + +import com.code_intelligence.jazzer.mutation.annotation.InRange; +import com.code_intelligence.jazzer.mutation.api.Debuggable; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.mutator.libfuzzer.LibFuzzerMutator; +import com.google.errorprone.annotations.ForOverride; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.ParameterizedType; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.LongStream; + +final class IntegralMutatorFactory extends MutatorFactory { + @Override + public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) { + if (!(type.getType() instanceof Class)) { + return Optional.empty(); + } + Class<?> clazz = (Class<?>) type.getType(); + + if (clazz == byte.class || clazz == Byte.class) { + return Optional.of(new AbstractIntegralMutator<Byte>(type, Byte.MIN_VALUE, Byte.MAX_VALUE) { + @Override + protected long mutateWithLibFuzzer(long value) { + return LibFuzzerMutator.mutateDefault((byte) value, this, 0); + } + + @Override + public Byte init(PseudoRandom prng) { + return (byte) initImpl(prng); + } + + @Override + public Byte mutate(Byte value, PseudoRandom prng) { + return (byte) mutateImpl(value, prng); + } + + @Override + public Byte crossOver(Byte value, Byte otherValue, PseudoRandom prng) { + return (byte) crossOverImpl(value, otherValue, prng); + } + + @Override + public Byte read(DataInputStream in) throws IOException { + return (byte) forceInRange(in.readByte()); + } + + @Override + public void write(Byte value, DataOutputStream out) throws IOException { + out.writeByte(value); + } + }); + } else if (clazz == short.class || clazz == Short.class) { + return Optional.of( + new AbstractIntegralMutator<Short>(type, Short.MIN_VALUE, Short.MAX_VALUE) { + @Override + protected long mutateWithLibFuzzer(long value) { + return LibFuzzerMutator.mutateDefault((short) value, this, 0); + } + + @Override + public Short init(PseudoRandom prng) { + return (short) initImpl(prng); + } + + @Override + public Short mutate(Short value, PseudoRandom prng) { + return (short) mutateImpl(value, prng); + } + + @Override + public Short crossOver(Short value, Short otherValue, PseudoRandom prng) { + return (short) crossOverImpl(value, otherValue, prng); + } + + @Override + public Short read(DataInputStream in) throws IOException { + return (short) forceInRange(in.readShort()); + } + + @Override + public void write(Short value, DataOutputStream out) throws IOException { + out.writeShort(value); + } + }); + } else if (clazz == int.class || clazz == Integer.class) { + return Optional.of( + new AbstractIntegralMutator<Integer>(type, Integer.MIN_VALUE, Integer.MAX_VALUE) { + @Override + protected long mutateWithLibFuzzer(long value) { + return LibFuzzerMutator.mutateDefault((int) value, this, 0); + } + + @Override + public Integer init(PseudoRandom prng) { + return (int) initImpl(prng); + } + + @Override + public Integer mutate(Integer value, PseudoRandom prng) { + return (int) mutateImpl(value, prng); + } + + @Override + public Integer crossOver(Integer value, Integer otherValue, PseudoRandom prng) { + return (int) crossOverImpl(value, otherValue, prng); + } + + @Override + public Integer read(DataInputStream in) throws IOException { + return (int) forceInRange(in.readInt()); + } + + @Override + public void write(Integer value, DataOutputStream out) throws IOException { + out.writeInt(value); + } + }); + } else if (clazz == long.class || clazz == Long.class) { + return Optional.of(new AbstractIntegralMutator<Long>(type, Long.MIN_VALUE, Long.MAX_VALUE) { + @Override + protected long mutateWithLibFuzzer(long value) { + return LibFuzzerMutator.mutateDefault(value, this, 0); + } + + @Override + public Long init(PseudoRandom prng) { + return initImpl(prng); + } + + @Override + public Long mutate(Long value, PseudoRandom prng) { + return mutateImpl(value, prng); + } + + @Override + public Long crossOver(Long value, Long otherValue, PseudoRandom prng) { + return crossOverImpl(value, otherValue, prng); + } + + @Override + public Long read(DataInputStream in) throws IOException { + return forceInRange(in.readLong()); + } + + @Override + public void write(Long value, DataOutputStream out) throws IOException { + out.writeLong(value); + } + }); + } else { + return Optional.empty(); + } + } + + // Based on + // https://github.com/google/fuzztest/blob/a663ded6c36f050fbdc634a8fc81d553068d71d7/fuzztest/internal/domain.h#L1447 + // SPDX: Apache-2.0 + // Copyright 2022 Google LLC + // + // Visible for testing. + static abstract class AbstractIntegralMutator<T extends Number> extends SerializingMutator<T> { + private static final long RANDOM_WALK_RANGE = 5; + private final long minValue; + private final long maxValue; + private final int largestMutableBitNegative; + private final int largestMutableBitPositive; + private final long[] specialValues; + + AbstractIntegralMutator( + AnnotatedType type, long defaultMinValueForType, long defaultMaxValueForType) { + long minValue = defaultMinValueForType; + long maxValue = defaultMaxValueForType; + // InRange is not repeatable, so the loop body will apply exactly once. + for (Annotation annotation : type.getAnnotations()) { + if (annotation instanceof InRange) { + InRange inRange = (InRange) annotation; + // Since we use a single annotation for all integral types and its min and max fields are + // longs, we have to ignore them if they are at their default values. + // + // This results in a small quirk that is probably acceptable: If someone specifies + // @InRange(max = Long.MAX_VALUE) on a byte, we will not fail but silently use + // Byte.MAX_VALUE instead. IDEs will warn about the redundant specification of the default + // value, so this should not be a problem in practice. + if (inRange.min() != Long.MIN_VALUE) { + require(inRange.min() >= defaultMinValueForType, + format("@InRange.min=%d is out of range: %s", inRange.min(), type.getType())); + minValue = inRange.min(); + } + if (inRange.max() != Long.MAX_VALUE) { + require(inRange.max() <= defaultMaxValueForType, + format("@InRange.max=%d is out of range: %s", inRange.max(), type.getType())); + maxValue = inRange.max(); + } + } + } + + require(minValue <= maxValue, + format("[%d, %d] is not a valid interval: %s", minValue, maxValue, type)); + require(minValue != maxValue, + format( + "[%d, %d] can not be mutated, use a constant instead: %s", minValue, maxValue, type)); + this.minValue = minValue; + this.maxValue = maxValue; + if (minValue >= 0) { + largestMutableBitNegative = 0; + largestMutableBitPositive = bitWidth(minValue ^ maxValue); + } else if (maxValue < 0) { + largestMutableBitNegative = bitWidth(minValue ^ maxValue); + largestMutableBitPositive = 0; + } else /* minValue < 0 && maxValue >= 0 */ { + largestMutableBitNegative = bitWidth(~minValue); + largestMutableBitPositive = bitWidth(maxValue); + } + this.specialValues = collectSpecialValues(minValue, maxValue); + } + + private static long[] collectSpecialValues(long minValue, long maxValue) { + // Special values can collide or not apply when @InRange is used, so filter appropriately and + // remove duplicates - we don't want to weigh certain special values higher than others. + return LongStream.of(0, 1, minValue, maxValue) + .filter(value -> value >= minValue) + .filter(value -> value <= maxValue) + .distinct() + .sorted() + .toArray(); + } + + private static int bitWidth(long value) { + return 64 - Long.numberOfLeadingZeros(value); + } + + protected final long initImpl(PseudoRandom prng) { + int sentinel = specialValues.length; + int choice = prng.closedRange(0, sentinel); + if (choice < sentinel) { + return specialValues[choice]; + } else { + return prng.closedRange(minValue, maxValue); + } + } + + protected final long mutateImpl(long value, PseudoRandom prng) { + final long previousValue = value; + // Mutate in a loop to verify that we really mutated. + do { + switch (prng.indexIn(4)) { + case 0: + value = bitFlip(value, prng); + break; + case 1: + value = randomWalk(value, prng); + break; + case 2: + value = prng.closedRange(minValue, maxValue); + break; + case 3: + // TODO: Replace this with a structure-aware dictionary/TORC search similar to fuzztest. + value = forceInRange(mutateWithLibFuzzer(value)); + break; + } + } while (value == previousValue); + return value; + } + + protected final long crossOverImpl(long x, long y, PseudoRandom prng) { + switch (prng.indexIn(3)) { + case 0: + return mean(x, y); + case 1: + return forceInRange(x ^ y); + case 2: + return bitmask(x, y, prng); + default: + throw new AssertionError("Invalid cross over function."); + } + } + + private long bitmask(long x, long y, PseudoRandom prng) { + long mask = prng.nextLong(); + return forceInRange((x & mask) | (y & ~mask)); + } + + private static long mean(long x, long y) { + // Add the common set bits (x & y) and the half of the sum of the + // differing bits together ((x ^ y) >> 1), the result will never exceed + // the sum of x and y as both parts of the calculation are guaranteed to + // be smaller than or equal to x and y. + long xor = x ^ y; + long mean = (x & y) + (xor >> 1); + // Round towards zero (add 1) if rounding is not exact (last xor bit is + // set) and result is negative (sign bit is set). + return mean + (1 & xor & (mean >>> 31)); + } + + @ForOverride protected abstract long mutateWithLibFuzzer(long value); + + /** + * Force value into the closed interval [minValue, maxValue] while preserving as many of its + * bits as possible (e.g. so that mutations that apply to the raw byte representation still have + * a good chance to actually mutate the value). Clamping would not have this property. + */ + protected final long forceInRange(long value) { + // Fast path for the common case. + if (value >= minValue && value <= maxValue) { + return value; + } + return forceInRange(value, minValue, maxValue); + } + + // Visible for testing. + static long forceInRange(long value, long minValue, long maxValue) { + long range = maxValue - minValue; + if (range > 0) { + return minValue + Math.abs((value - minValue) % range); + } else { + // [minValue, maxValue] covers at least half of the [Long.MIN_VALUE, Long.MAX_VALUE] range, + // so if value doesn't lie in [minValue, maxValue], it will after shifting once. + if (value >= minValue && value <= maxValue) { + return value; + } else { + return value + range; + } + } + } + + private long bitFlip(long value, PseudoRandom prng) { + int range = value >= 0 ? largestMutableBitPositive : largestMutableBitNegative; + value = value ^ (1L << prng.indexIn(range)); + // The bit flip may violate the range constraint, if so, mutate randomly. + if (value > maxValue || value < minValue) { + value = prng.closedRange(minValue, maxValue); + } + return value; + } + + private long randomWalk(long value, PseudoRandom prng) { + // Prevent overflows by averaging the individual bounds. + if (maxValue / 2 - minValue / 2 <= RANDOM_WALK_RANGE) { + value = prng.closedRange(minValue, maxValue); + } else { + // At this point we know that (using non-wrapping arithmetic): + // RANDOM_WALK_RANGE < maxValue/2 - minValue/2 <= Long.MAX_VALUE/2 - minValue/2, hence + // minValue/2 + RANDOM_WALK_RANGE < Long.MAX_VALUE/2, hence + // minValue + 2*RANDOM_WALK_RANGE < Long.MAX_VALUE. + // In particular, minValue + RANDOM_WALK_RANGE can't overflow, likewise for maxValue. + long lower = minValue; + if (value > lower + RANDOM_WALK_RANGE) { + lower = value - RANDOM_WALK_RANGE; + } + long upper = maxValue; + if (value < upper - RANDOM_WALK_RANGE) { + upper = value + RANDOM_WALK_RANGE; + } + value = prng.closedRange(lower, upper); + } + return value; + } + + @Override + public T detach(T value) { + // Always immutable. + return value; + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return ((Class<T>) ((ParameterizedType) this.getClass().getGenericSuperclass()) + .getActualTypeArguments()[0]) + .getSimpleName(); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java new file mode 100644 index 00000000..00ea8b47 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java @@ -0,0 +1,30 @@ +/* + * 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.mutation.mutator.lang; + +import com.code_intelligence.jazzer.mutation.api.ChainedMutatorFactory; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; + +public final class LangMutators { + private LangMutators() {} + + public static MutatorFactory newFactory() { + return new ChainedMutatorFactory(new NullableMutatorFactory(), new BooleanMutatorFactory(), + new FloatingPointMutatorFactory(), new IntegralMutatorFactory(), + new ByteArrayMutatorFactory(), new StringMutatorFactory(), new EnumMutatorFactory()); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/NullableMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/NullableMutatorFactory.java new file mode 100644 index 00000000..16e6a132 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/NullableMutatorFactory.java @@ -0,0 +1,126 @@ +/* + * 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.mutation.mutator.lang; + +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.isPrimitive; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.notNull; +import static java.util.Arrays.stream; + +import com.code_intelligence.jazzer.mutation.api.Debuggable; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; +import java.util.Optional; +import java.util.function.Predicate; + +final class NullableMutatorFactory extends MutatorFactory { + private static boolean isNotNullAnnotation(Annotation annotation) { + // There are many NotNull annotations in the wild (including our own) and we want to recognize + // them all. + return annotation.annotationType().getSimpleName().equals("NotNull"); + } + + @Override + public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) { + if (isPrimitive(type) + || stream(type.getAnnotations()).anyMatch(NullableMutatorFactory::isNotNullAnnotation)) { + return Optional.empty(); + } + return factory.tryCreate(notNull(type), factory).map(NullableMutator::new); + } + + private static final class NullableMutator<T> extends SerializingMutator<T> { + private static final int INVERSE_FREQUENCY_NULL = 100; + + private final SerializingMutator<T> mutator; + + NullableMutator(SerializingMutator<T> mutator) { + this.mutator = mutator; + } + + @Override + public T read(DataInputStream in) throws IOException { + if (in.readBoolean()) { + return mutator.read(in); + } else { + return null; + } + } + + @Override + public void write(T value, DataOutputStream out) throws IOException { + out.writeBoolean(value != null); + if (value != null) { + mutator.write(value, out); + } + } + + @Override + public T init(PseudoRandom prng) { + if (prng.trueInOneOutOf(INVERSE_FREQUENCY_NULL)) { + return null; + } else { + return mutator.init(prng); + } + } + + @Override + public T mutate(T value, PseudoRandom prng) { + if (value == null) { + return mutator.init(prng); + } else if (prng.trueInOneOutOf(INVERSE_FREQUENCY_NULL)) { + return null; + } else { + return mutator.mutate(value, prng); + } + } + + @Override + public T crossOver(T value, T otherValue, PseudoRandom prng) { + // Prefer to cross over actual values and only return null if + // both are null or at INVERSE_FREQUENCY_NULL probability. + if (value != null && otherValue != null) { + return mutator.crossOver(value, otherValue, prng); + } else if (value == null && otherValue == null) { + return null; + } else if (prng.trueInOneOutOf(INVERSE_FREQUENCY_NULL)) { + return null; + } else { + return value != null ? value : otherValue; + } + } + + @Override + public T detach(T value) { + if (value == null) { + return null; + } else { + return mutator.detach(value); + } + } + + @Override + public String toDebugString(Predicate<Debuggable> isInCycle) { + return "Nullable<" + mutator.toDebugString(isInCycle) + ">"; + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java new file mode 100644 index 00000000..d77cb9d3 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java @@ -0,0 +1,172 @@ +/* + * 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.mutation.mutator.lang; + +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.*; + +import com.code_intelligence.jazzer.mutation.annotation.Ascii; +import com.code_intelligence.jazzer.mutation.annotation.WithUtf8Length; +import com.code_intelligence.jazzer.mutation.api.Debuggable; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import java.lang.reflect.AnnotatedType; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.function.Predicate; + +final class StringMutatorFactory extends MutatorFactory { + private static final int HEADER_MASK = 0b1100_0000; + private static final int BODY_MASK = 0b0011_1111; + private static final int CONTINUATION_HEADER = 0b1000_0000; + + private static final int DEFAULT_MIN_BYTES = 0; + + private static final int DEFAULT_MAX_BYTES = 1000; + + static void fixUpAscii(byte[] bytes) { + for (int i = 0; i < bytes.length; i++) { + bytes[i] &= 0x7F; + } + } + + // Based on + // https://github.com/google/libprotobuf-mutator/blob/af3bb18749db3559dc4968dd85319d05168d4b5e/src/utf8_fix.cc#L32 + // SPDX: Apache-2.0 + // Copyright 2022 Google LLC + static void fixUpUtf8(byte[] bytes) { + for (int pos = 0; pos < bytes.length;) { + // Leniently read a UTF-8 code point consisting of any byte viewed as the leading byte and up + // to three following bytes that have a continuation byte header. + // + // Since the upper two bits of a byte are 10 with probability 25%, this roughly results in + // the following distribution for characters: + // + // ASCII code point: 75% + // two-byte UTF-8: 18.75% + // three-byte UTF-8: ~4.7% + // four-byte UTF-8: ~1.2% + int scanPos = pos + 1; + int maxScanPos = Math.min(pos + 4, bytes.length); + + int codePoint = bytes[pos] & 0xFF; + for (; scanPos < maxScanPos; scanPos++) { + byte b = bytes[scanPos]; + if ((b & HEADER_MASK) != CONTINUATION_HEADER) { + break; + } + codePoint = (codePoint << 6) + (b & BODY_MASK); + } + + int size = scanPos - pos; + int nextPos = scanPos; + switch (size) { + case 1: + // Force code point to be ASCII. + codePoint &= 0x7F; + + bytes[pos] = (byte) codePoint; + break; + case 2: + codePoint &= 0x7FF; + if (codePoint <= 0x7F) { + // The code point encoding must not be longer than necessary, so fix up the code point + // to actually require two bytes without fixing too many bits. + codePoint |= 0x80; + } + + bytes[--scanPos] = (byte) (CONTINUATION_HEADER | (codePoint & BODY_MASK)); + codePoint >>= 6; + bytes[pos] = (byte) (0b1100_0000 | codePoint); + break; + case 3: + codePoint &= 0xFFFF; + if (codePoint <= 0x7FF) { + // The code point encoding must not be longer than necessary, so fix up the code point + // to actually require three bytes without fixing too many bits. + codePoint |= 0x800; + } + if (codePoint >= 0xD800 && codePoint <= 0xDFFF) { + // The code point must not be a low or high UTF-16 surrogate pair, which are not allowed + // in UTF-8. + codePoint |= (codePoint & ~0xF000) | 0xE000; + } + + bytes[--scanPos] = (byte) (CONTINUATION_HEADER | (codePoint & BODY_MASK)); + codePoint >>= 6; + bytes[--scanPos] = (byte) (CONTINUATION_HEADER | (codePoint & BODY_MASK)); + codePoint >>= 6; + bytes[pos] = (byte) (0b1110_0000 | codePoint); + break; + case 4: + codePoint &= 0x1FFFFF; + if (codePoint <= 0xFFFF) { + // The code point encoding must not be longer than necessary, so fix up the code point + // to actually require four bytes without fixing too many bits. + codePoint |= 0x100000; + } + if (codePoint > 0x10FFFF) { + // The code point must be in the valid Unicode range, so fix it up by clearing as few + // bits as possible. + codePoint &= ~0x10FFFF; + } + + bytes[--scanPos] = (byte) (CONTINUATION_HEADER | (codePoint & BODY_MASK)); + codePoint >>= 6; + bytes[--scanPos] = (byte) (CONTINUATION_HEADER | (codePoint & BODY_MASK)); + codePoint >>= 6; + bytes[--scanPos] = (byte) (CONTINUATION_HEADER | (codePoint & BODY_MASK)); + codePoint >>= 6; + bytes[pos] = (byte) (0b1111_0000 | codePoint); + break; + default: + throw new IllegalStateException("Not reached as scanPos <= pos + 4"); + } + + pos = nextPos; + } + } + + @Override + public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) { + Optional<WithUtf8Length> utf8Length = + Optional.ofNullable(type.getAnnotation(WithUtf8Length.class)); + int min = utf8Length.map(WithUtf8Length::min).orElse(DEFAULT_MIN_BYTES); + int max = utf8Length.map(WithUtf8Length::max).orElse(DEFAULT_MAX_BYTES); + + AnnotatedType innerByteArray = notNull(withLength(asAnnotatedType(byte[].class), min, max)); + + return findFirstParentIfClass(type, String.class) + .flatMap(parent -> factory.tryCreate(innerByteArray)) + .map(byteArrayMutator -> { + boolean fixUpAscii = type.getDeclaredAnnotation(Ascii.class) != null; + return mutateThenMapToImmutable((SerializingMutator<byte[]>) byteArrayMutator, + bytes + -> { + if (fixUpAscii) { + fixUpAscii(bytes); + } else { + fixUpUtf8(bytes); + } + return new String(bytes, StandardCharsets.UTF_8); + }, + string + -> string.getBytes(StandardCharsets.UTF_8), + (Predicate<Debuggable> inCycle) -> "String"); + }); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer/BUILD.bazel new file mode 100644 index 00000000..284264bb --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer/BUILD.bazel @@ -0,0 +1,15 @@ +java_library( + name = "libfuzzer", + srcs = ["LibFuzzerMutator.java"], + visibility = [ + # libFuzzer's mutators should only by used by mutators for primitive types as we want to get + # rid of this dependency eventually. + "//src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang:__pkg__", + "//src/test/java/com/code_intelligence/jazzer/mutation/mutator/lang:__subpackages__", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/api", + "//src/main/java/com/code_intelligence/jazzer/mutation/support", + "//src/main/java/com/code_intelligence/jazzer/runtime:mutator", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer/LibFuzzerMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer/LibFuzzerMutator.java new file mode 100644 index 00000000..c77c75e5 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer/LibFuzzerMutator.java @@ -0,0 +1,92 @@ +/* + * 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.mutation.mutator.libfuzzer; + +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; + +import com.code_intelligence.jazzer.mutation.api.Serializer; +import com.code_intelligence.jazzer.runtime.Mutator; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; + +public final class LibFuzzerMutator { + /** + * Key name to give to {@link System#setProperty(String, String)} to control the size of the + * returned array for {@link #defaultMutateMock(byte[], int)}. Only used for testing purposes. + */ + public static final String MOCK_SIZE_KEY = "libfuzzermutator.mock.newsize"; + + public static byte[] mutateDefault(byte[] data, int maxSizeIncrease) { + byte[] mutatedBytes; + if (maxSizeIncrease == 0) { + mutatedBytes = data; + } else { + mutatedBytes = Arrays.copyOf(data, data.length + maxSizeIncrease); + } + int newSize = defaultMutate(mutatedBytes, data.length); + if (newSize == 0) { + // Mutation failed. This should happen very rarely. + return data; + } + return Arrays.copyOf(mutatedBytes, newSize); + } + + public static <T> T mutateDefault(T value, Serializer<T> serializer, int maxSizeIncrease) { + require(maxSizeIncrease >= 0); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + serializer.writeExclusive(value, out); + } catch (IOException e) { + throw new IllegalStateException( + "writeExclusive is not expected to throw if the underlying stream doesn't", e); + } + + byte[] mutatedBytes = mutateDefault(out.toByteArray(), maxSizeIncrease); + + try { + return serializer.readExclusive(new ByteArrayInputStream(mutatedBytes)); + } catch (IOException e) { + throw new IllegalStateException( + "readExclusive is not expected to throw if the underlying stream doesn't", e); + } + } + + private static int defaultMutate(byte[] buffer, int size) { + if (Mutator.SHOULD_MOCK) { + return defaultMutateMock(buffer, size); + } else { + return Mutator.defaultMutateNative(buffer, size); + } + } + + private static int defaultMutateMock(byte[] buffer, int size) { + String newSizeProp = System.getProperty(MOCK_SIZE_KEY); + int newSize = Math.min(buffer.length, size + 1); + if (newSizeProp != null) { + newSize = Integer.parseUnsignedInt(newSizeProp); + } + + for (int i = 0; i < newSize; i++) { + buffer[i] += i + 1; + } + return newSize; + } + + private LibFuzzerMutator() {} +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BUILD.bazel new file mode 100644 index 00000000..2c591709 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BUILD.bazel @@ -0,0 +1,16 @@ +java_library( + name = "proto", + srcs = glob(["*.java"]), + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/mutator:__pkg__", + "//src/test/java/com/code_intelligence/jazzer/mutation/mutator/proto:__pkg__", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", + "//src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto", + "//src/main/java/com/code_intelligence/jazzer/mutation/annotation/proto:protobuf_runtime_compile_only", + "//src/main/java/com/code_intelligence/jazzer/mutation/api", + "//src/main/java/com/code_intelligence/jazzer/mutation/combinator", + "//src/main/java/com/code_intelligence/jazzer/mutation/support", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderAdapters.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderAdapters.java new file mode 100644 index 00000000..12dcd406 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderAdapters.java @@ -0,0 +1,188 @@ +/* + * 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.mutation.mutator.proto; + +import static java.util.Collections.singletonList; + +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Message; +import com.google.protobuf.Message.Builder; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +final class BuilderAdapters { + private BuilderAdapters() {} + + static <T extends Builder, U> List<U> makeMutableRepeatedFieldView( + T builder, FieldDescriptor field) { + return new AbstractList<U>() { + // O(1) + @Override + public U get(int index) { + return (U) builder.getRepeatedField(field, index); + } + + // O(1) + @Override + public int size() { + return builder.getRepeatedFieldCount(field); + } + + // O(1) + @Override + public boolean add(U element) { + builder.addRepeatedField(field, element); + return true; + } + + // O(1) + @Override + public void add(int index, U element) { + addAll(index, singletonList(element)); + } + + // O(size() + other.size()) + public boolean addAll(int index, Collection<? extends U> other) { + // This was benchmarked against the following implementation and found to be faster in all + // cases (up to 4x on lists of size 1000): + // + // for (U element : other) { + // builder.addRepeatedField(field, element); + // } + // Collections.rotate(subList(index, size()), other.size()); + int otherSize = other.size(); + if (otherSize == 0) { + return false; + } + + int originalSize = size(); + if (index == originalSize) { + for (U element : other) { + builder.addRepeatedField(field, element); + } + return true; + } + + int newSize = originalSize + otherSize; + ArrayList<U> temp = new ArrayList<>(newSize); + for (int i = 0; i < index; i++) { + temp.add((U) builder.getRepeatedField(field, i)); + } + temp.addAll(other); + for (int i = index; i < originalSize; i++) { + temp.add((U) builder.getRepeatedField(field, i)); + } + + replaceWith(temp); + return true; + } + + // O(1) + @Override + public U set(int index, U element) { + U previous = get(index); + builder.setRepeatedField(field, index, element); + return previous; + } + + // O(size()) + @Override + public U remove(int index) { + U removed = get(index); + removeRange(index, index + 1); + return removed; + } + + // O(size() - (toIndex - fromIndex)) + @Override + protected void removeRange(int fromIndex, int toIndex) { + int originalSize = size(); + int newSize = originalSize - (toIndex - fromIndex); + if (newSize == 0) { + builder.clearField(field); + return; + } + + // There is no way to remove individual repeated field entries without clearing the entire + // field, so we have to iterate over all entries and keep them in a temporary list. + ArrayList<U> temp = new ArrayList<>(newSize); + for (int i = 0; i < fromIndex; i++) { + temp.add((U) builder.getRepeatedField(field, i)); + } + for (int i = toIndex; i < originalSize; i++) { + temp.add((U) builder.getRepeatedField(field, i)); + } + + replaceWith(temp); + } + + private void replaceWith(ArrayList<U> temp) { + builder.clearField(field); + for (U element : temp) { + builder.addRepeatedField(field, element); + } + } + }; + } + + static <T extends Builder, U> U getPresentFieldOrNull(T builder, FieldDescriptor field) { + if (builder.hasField(field)) { + return (U) builder.getField(field); + } else { + return null; + } + } + + static <T extends Builder, U> void setFieldWithPresence( + T builder, FieldDescriptor field, U value) { + if (value == null) { + builder.clearField(field); + } else { + builder.setField(field, value); + } + } + + static <T extends Builder, K, V> Map<K, V> getMapField(T builder, FieldDescriptor field) { + int size = builder.getRepeatedFieldCount(field); + FieldDescriptor keyField = field.getMessageType().getFields().get(0); + FieldDescriptor valueField = field.getMessageType().getFields().get(1); + HashMap<K, V> map = new HashMap<>(size); + for (int i = 0; i < size; i++) { + Message entry = (Message) builder.getRepeatedField(field, i); + map.put((K) entry.getField(keyField), (V) entry.getField(valueField)); + } + return map; + } + + static <T extends Builder, K, V> void setMapField( + Builder builder, FieldDescriptor field, Map<K, V> map) { + builder.clearField(field); + FieldDescriptor keyField = field.getMessageType().getFields().get(0); + FieldDescriptor valueField = field.getMessageType().getFields().get(1); + Builder entryBuilder = builder.newBuilderForField(field); + for (Entry<K, V> entry : map.entrySet()) { + entryBuilder.setField(keyField, entry.getKey()); + entryBuilder.setField(valueField, entry.getValue()); + builder.addRepeatedField(field, entryBuilder.build()); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java new file mode 100644 index 00000000..85427c6f --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java @@ -0,0 +1,451 @@ +/* + * 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.mutation.mutator.proto; + +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.assemble; +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.combine; +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.fixedValue; +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateIndices; +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateProperty; +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateSumInPlace; +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable; +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateViaView; +import static com.code_intelligence.jazzer.mutation.mutator.proto.BuilderAdapters.getMapField; +import static com.code_intelligence.jazzer.mutation.mutator.proto.BuilderAdapters.getPresentFieldOrNull; +import static com.code_intelligence.jazzer.mutation.mutator.proto.BuilderAdapters.makeMutableRepeatedFieldView; +import static com.code_intelligence.jazzer.mutation.mutator.proto.BuilderAdapters.setFieldWithPresence; +import static com.code_intelligence.jazzer.mutation.mutator.proto.BuilderAdapters.setMapField; +import static com.code_intelligence.jazzer.mutation.mutator.proto.TypeLibrary.getDefaultInstance; +import static com.code_intelligence.jazzer.mutation.mutator.proto.TypeLibrary.withoutInitIfRecursive; +import static com.code_intelligence.jazzer.mutation.support.InputStreamSupport.cap; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asAnnotatedType; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asSubclassOrEmpty; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.findFirstParentIfClass; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.notNull; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.withExtraAnnotations; +import static java.util.Arrays.stream; +import static java.util.Objects.requireNonNull; +import static java.util.function.UnaryOperator.identity; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; + +import com.code_intelligence.jazzer.mutation.annotation.proto.AnySource; +import com.code_intelligence.jazzer.mutation.annotation.proto.WithDefaultInstance; +import com.code_intelligence.jazzer.mutation.api.ChainedMutatorFactory; +import com.code_intelligence.jazzer.mutation.api.InPlaceMutator; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.Serializer; +import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.support.Preconditions; +import com.google.protobuf.Any; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.EnumDescriptor; +import com.google.protobuf.Descriptors.EnumValueDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; +import com.google.protobuf.Descriptors.OneofDescriptor; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.protobuf.Message.Builder; +import com.google.protobuf.UnknownFieldSet; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public final class BuilderMutatorFactory extends MutatorFactory { + private <T extends Builder, U> InPlaceMutator<T> mutatorForField( + FieldDescriptor field, Annotation[] annotations, MutatorFactory factory) { + factory = withDescriptorDependentMutatorFactoryIfNeeded(factory, field, annotations); + AnnotatedType typeToMutate = TypeLibrary.getTypeToMutate(field); + requireNonNull(typeToMutate, () -> "Java class not specified for " + field); + + InPlaceMutator<T> mutator; + if (field.isMapField()) { + SerializingInPlaceMutator<Map> underlyingMutator = + (SerializingInPlaceMutator<Map>) factory.createInPlaceOrThrow(typeToMutate); + mutator = mutateProperty(builder + -> getMapField(builder, field), + underlyingMutator, (builder, value) -> setMapField(builder, field, value)); + } else if (field.isRepeated()) { + SerializingInPlaceMutator<List<U>> underlyingMutator = + (SerializingInPlaceMutator<List<U>>) factory.createInPlaceOrThrow(typeToMutate); + mutator = + mutateViaView(builder -> makeMutableRepeatedFieldView(builder, field), underlyingMutator); + } else if (field.hasPresence()) { + SerializingMutator<U> underlyingMutator = + (SerializingMutator<U>) factory.createOrThrow(typeToMutate); + mutator = mutateProperty(builder + -> getPresentFieldOrNull(builder, field), + underlyingMutator, (builder, value) -> setFieldWithPresence(builder, field, value)); + } else { + SerializingMutator<U> underlyingMutator = + (SerializingMutator<U>) factory.createOrThrow(typeToMutate); + mutator = mutateProperty(builder + -> (U) builder.getField(field), + underlyingMutator, (builder, value) -> builder.setField(field, value)); + } + + // If recursive message fields (i.e. those that have themselves as transitive subfields) are + // initialized eagerly, they tend to nest very deeply, which easily results in stack overflows. + // We guard against that by making their init a no-op and instead initialize them layer by layer + // in mutations. + return withoutInitIfRecursive(mutator, field); + } + + private MutatorFactory withDescriptorDependentMutatorFactoryIfNeeded( + MutatorFactory originalFactory, FieldDescriptor field, Annotation[] annotations) { + if (field.getJavaType() == JavaType.ENUM) { + // Proto enum fields are special as their type (EnumValueDescriptor) does not encode their + // domain - we need the actual EnumDescriptor instance. + return new ChainedMutatorFactory(originalFactory, new MutatorFactory() { + @Override + public Optional<SerializingMutator<?>> tryCreate( + AnnotatedType type, MutatorFactory factory) { + return findFirstParentIfClass(type, EnumValueDescriptor.class).map(parent -> { + EnumDescriptor enumType = field.getEnumType(); + List<EnumValueDescriptor> values = enumType.getValues(); + String name = enumType.getName(); + if (values.size() == 1) { + // While we generally prefer to error out instead of creating a mutator that can't + // actually mutate its domain, we can't do that for proto enum fields as the user + // creating the fuzz test may not be in a position to modify the existing proto + // definition. + return fixedValue(values.get(0)); + } else { + return mutateThenMapToImmutable(mutateIndices(values.size()), values::get, + EnumValueDescriptor::getIndex, unused -> "Enum<" + name + ">"); + } + }); + } + }); + } else if (field.getJavaType() == JavaType.MESSAGE) { + Descriptor messageDescriptor; + if (field.isMapField()) { + // Map fields are represented as messages, but we mutate them as actual Java Maps. In case + // the values of the proto map are themselves messages, we need to mutate their type. + FieldDescriptor valueField = field.getMessageType().getFields().get(1); + if (valueField.getJavaType() != JavaType.MESSAGE) { + return originalFactory; + } + messageDescriptor = valueField.getMessageType(); + } else { + messageDescriptor = field.getMessageType(); + } + return new ChainedMutatorFactory(originalFactory, new MutatorFactory() { + @Override + public Optional<SerializingMutator<?>> tryCreate( + AnnotatedType type, MutatorFactory factory) { + return asSubclassOrEmpty(type, Message.Builder.class).flatMap(clazz -> { + // BuilderMutatorFactory only handles subclasses of Message.Builder and requests + // Message.Builder itself for message fields, which we handle here. + if (clazz != Message.Builder.class) { + return Optional.empty(); + } + // It is important that we use originalFactory here instead of factory: factory has this + // field-specific message mutator appended, but this mutator should only be used for + // this particular field and not any message subfields. + return Optional.of(makeBuilderMutator(originalFactory, + DynamicMessage.getDefaultInstance(messageDescriptor), annotations)); + }); + } + }); + } else { + return originalFactory; + } + } + + private <T extends Builder> Stream<InPlaceMutator<T>> mutatorsForFields( + Optional<OneofDescriptor> oneofField, List<FieldDescriptor> fields, Annotation[] annotations, + MutatorFactory factory) { + if (oneofField.isPresent()) { + // oneof fields are mutated as one as mutating them independently would cause the mutator to + // erratically switch between the different states. The individual fields are kept in the + // order in which they are defined in the .proto file. + OneofDescriptor oneofDescriptor = oneofField.get(); + + IdentityHashMap<FieldDescriptor, Integer> indexInOneof = + new IdentityHashMap<>(oneofDescriptor.getFieldCount()); + for (int i = 0; i < oneofDescriptor.getFieldCount(); i++) { + indexInOneof.put(oneofDescriptor.getField(i), i); + } + + return Stream.of(mutateSumInPlace( + (T builder) + -> { + FieldDescriptor setField = builder.getOneofFieldDescriptor(oneofDescriptor); + if (setField == null) { + return -1; + } else { + return indexInOneof.get(setField); + } + }, + // Mutating to the unset (-1) state is handled by the individual field mutators, which + // are created nullable as oneof fields report that they track presence. + fields.stream() + .map(field -> mutatorForField(field, annotations, factory)) + .toArray(InPlaceMutator[] ::new))); + } else { + // All non-oneof fields are mutated independently, using the order in which they are declared + // in the .proto file (which may not coincide with the order by field number). + return fields.stream().map(field -> mutatorForField(field, annotations, factory)); + } + } + + private static <M extends Message, B extends Builder> Serializer<B> makeBuilderSerializer( + M defaultInstance) { + return new Serializer<B>() { + @Override + public B read(DataInputStream in) throws IOException { + int length = Math.max(in.readInt(), 0); + return (B) parseLeniently(cap(in, length)); + } + + @Override + public B readExclusive(InputStream in) throws IOException { + return (B) parseLeniently(in); + } + + private Builder parseLeniently(InputStream in) throws IOException { + Builder builder = defaultInstance.toBuilder(); + try { + builder.mergeFrom(in); + } catch (InvalidProtocolBufferException ignored) { + // builder has been partially modified with what could be decoded before the parser error. + } + // We never want the fuzz test to see unknown fields and our mutations should never produce + // them. + builder.setUnknownFields(UnknownFieldSet.getDefaultInstance()); + // Required fields may not have been set at this point. We set them to default values to + // prevent an exception when built. + forceInitialized(builder); + return builder; + } + + private void forceInitialized(Builder builder) { + if (builder.isInitialized()) { + return; + } + for (FieldDescriptor field : builder.getDescriptorForType().getFields()) { + if (!field.isRequired()) { + continue; + } + if (field.getJavaType() == JavaType.MESSAGE) { + forceInitialized(builder.getFieldBuilder(field)); + } else if (!builder.hasField(field)) { + builder.setField(field, field.getDefaultValue()); + } + } + } + + @Override + public void write(Builder builder, DataOutputStream out) throws IOException { + Message message = builder.build(); + out.writeInt(message.getSerializedSize()); + message.writeTo(out); + } + + @Override + public void writeExclusive(Builder builder, OutputStream out) throws IOException { + builder.build().writeTo(out); + } + + @Override + public B detach(Builder builder) { + return (B) builder.build().toBuilder(); + } + }; + } + + /* + * Ensures that only a single instance is created per builder class and shared among all mutators + * that need it. This ensures that arbitrarily nested recursive structures such as a Protobuf + * message type that contains itself as a message field are representable as fixed-size mutator + * structures. + * + * Note: The resulting mutator structures may no longer form a tree: If A is a protobuf message + * type with a message field B and B in turn has a message field of type A, then the mutators for + * A and B will reference each other, forming a cycle. + */ + private final HashMap<CacheKey, SerializingMutator<? extends Builder>> internedMutators = + new HashMap<>(); + + private SerializingMutator<Any.Builder> mutatorForAny( + AnySource anySource, MutatorFactory factory) { + Map<String, Integer> typeUrlToIndex = + IntStream.range(0, anySource.value().length) + .boxed() + .collect(toMap(i -> getTypeUrl(getDefaultInstance(anySource.value()[i])), identity())); + + return assemble(mutator + -> internedMutators.put(new CacheKey(Any.getDescriptor(), anySource), mutator), + Any.getDefaultInstance()::toBuilder, makeBuilderSerializer(Any.getDefaultInstance()), + () + -> mutateSumInPlace( + // Corpus entries may contain Anys with arbitrary (and even invalid) messages, so we + // fall back to mutating the first message type if the type isn't recognized. + (Any.Builder builder) + -> typeUrlToIndex.getOrDefault(builder.getTypeUrl(), 0), + stream(anySource.value()) + .map(messageClass -> { + SerializingMutator<Message> messageMutator = + (SerializingMutator<Message>) factory.createOrThrow(notNull( + withExtraAnnotations(asAnnotatedType(messageClass), anySource))); + return mutateProperty( + (Any.Builder anyBuilder) + -> { + try { + return anyBuilder.build().unpack(messageClass); + } catch (InvalidProtocolBufferException e) { + // This can only happen if the corpus contains an invalid Any. + return getDefaultInstance(messageClass); + } + }, + messageMutator, + (Any.Builder any, Message message) -> { + any.setTypeUrl(getTypeUrl(message)); + any.setValue(message.toByteString()); + }); + }) + .toArray(InPlaceMutator[] ::new))); + } + + private static String getTypeUrl(Message message) { + // We only support the default "type.googleapis.com" prefix. + // https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/any.proto#L94 + return "type.googleapis.com/" + message.getDescriptorForType().getFullName(); + } + + @Override + public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) { + return asSubclassOrEmpty(type, Builder.class).flatMap(builderClass -> { + Message defaultInstance; + WithDefaultInstance withDefaultInstance = type.getAnnotation(WithDefaultInstance.class); + if (withDefaultInstance != null) { + defaultInstance = getDefaultInstance(withDefaultInstance); + } else if (builderClass == DynamicMessage.Builder.class) { + throw new IllegalArgumentException( + "To mutate a dynamic message, add a @WithDefaultInstance annotation specifying the" + + " fully qualified method name of a static method returning a default instance"); + } else if (builderClass == Message.Builder.class) { + // Handled by a custom mutator factory for message fields that is created in + // withDescriptorDependentMutatorFactoryIfNeeded. Without @WithDefaultInstance, + // BuilderMutatorFactory only handles proper subclasses, which correspond to generated + // message types. + return Optional.empty(); + } else { + defaultInstance = + getDefaultInstance((Class<? extends Message>) builderClass.getEnclosingClass()); + } + + return Optional.of( + makeBuilderMutator(factory, defaultInstance, type.getDeclaredAnnotations())); + }); + } + + private SerializingMutator<?> makeBuilderMutator( + MutatorFactory factory, Message defaultInstance, Annotation[] annotations) { + AnySource anySource = (AnySource) stream(annotations) + .filter(annotation -> annotation.annotationType() == AnySource.class) + .findFirst() + .orElse(null); + Preconditions.require(anySource == null || anySource.value().length > 0, + "@AnySource must list a non-empty list of classes"); + Descriptor descriptor = defaultInstance.getDescriptorForType(); + + CacheKey cacheKey = new CacheKey(descriptor, anySource); + if (internedMutators.containsKey(cacheKey)) { + return internedMutators.get(cacheKey); + } + + // If there is no @AnySource, mutate the Any.Builder fields just like a regular message. + // TODO: Determine whether we should show a warning in this case. + if (descriptor.equals(Any.getDescriptor()) && anySource != null) { + return mutatorForAny(anySource, factory); + } + + // assemble inserts the instance of the newly created builder mutator into the + // internedMutators map *before* recursively creating the mutators for its fields, which + // ensures that the recursion is finite (bounded by the total number of distinct message types + // that transitively occur as field types on the current message type). + return assemble(mutator + -> internedMutators.put(cacheKey, mutator), + defaultInstance::toBuilder, makeBuilderSerializer(defaultInstance), + () + -> combine( + descriptor.getFields() + .stream() + // Keep oneofs sorted by the first appearance of their fields in the + // .proto file. + .collect(groupingBy( + // groupingBy does not support null keys. We use getRealContainingOneof() + // instead of getContainingOneof() as the latter also reports oneofs for + // proto3 optional fields, which we handle separately. + fieldDescriptor + -> Optional.ofNullable(fieldDescriptor.getRealContainingOneof()), + LinkedHashMap::new, toList())) + .entrySet() + .stream() + .flatMap(entry + -> mutatorsForFields(entry.getKey(), entry.getValue(), + anySource == null ? new Annotation[0] : new Annotation[] {anySource}, + factory)) + .toArray(InPlaceMutator[] ::new))); + } + + private static final class CacheKey { + private final Descriptor descriptor; + private final AnySource anySource; + + private CacheKey(Descriptor descriptor, AnySource anySource) { + this.descriptor = descriptor; + this.anySource = anySource; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CacheKey cacheKey = (CacheKey) o; + return descriptor == cacheKey.descriptor && Objects.equals(anySource, cacheKey.anySource); + } + + @Override + public int hashCode() { + return 31 * System.identityHashCode(descriptor) + Objects.hashCode(anySource); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ByteStringMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ByteStringMutatorFactory.java new file mode 100644 index 00000000..a01ffae6 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ByteStringMutatorFactory.java @@ -0,0 +1,41 @@ +/* + * 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.mutation.mutator.proto; + +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asAnnotatedType; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.findFirstParentIfClass; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.notNull; + +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.google.protobuf.ByteString; +import java.lang.reflect.AnnotatedType; +import java.util.Optional; + +final class ByteStringMutatorFactory extends MutatorFactory { + ByteStringMutatorFactory() {} + + @Override + public Optional<SerializingMutator<?>> tryCreate(AnnotatedType type, MutatorFactory factory) { + return findFirstParentIfClass(type, ByteString.class) + .flatMap(parent -> factory.tryCreate(notNull(asAnnotatedType(byte[].class)))) + .map(byteArrayMutator + -> mutateThenMapToImmutable((SerializingMutator<byte[]>) byteArrayMutator, + ByteString::copyFrom, ByteString::toByteArray)); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/MessageMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/MessageMutatorFactory.java new file mode 100644 index 00000000..eb3220f5 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/MessageMutatorFactory.java @@ -0,0 +1,52 @@ +/* + * 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.mutation.mutator.proto; + +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asAnnotatedType; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asSubclassOrEmpty; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.withExtraAnnotations; + +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.google.protobuf.Message; +import com.google.protobuf.Message.Builder; +import java.lang.reflect.AnnotatedType; +import java.util.Arrays; +import java.util.Optional; + +public final class MessageMutatorFactory extends MutatorFactory { + @Override + public Optional<SerializingMutator<?>> tryCreate( + AnnotatedType messageType, MutatorFactory factory) { + return asSubclassOrEmpty(messageType, Message.class) + // If the Message class doesn't have a nested Builder class, it is not a concrete generated + // message and we can't mutate it. + .flatMap(messageClass + -> Arrays.stream(messageClass.getDeclaredClasses()) + .filter(clazz -> clazz.getSimpleName().equals("Builder")) + .findFirst()) + .flatMap(builderClass + -> + // Forward the annotations (e.g. @NotNull) on the Message type to the Builder type. + factory.tryCreateInPlace( + withExtraAnnotations(asAnnotatedType(builderClass), messageType.getAnnotations()))) + .map(builderMutator + -> mutateThenMapToImmutable( + (SerializingMutator<Builder>) builderMutator, Builder::build, Message::toBuilder)); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java new file mode 100644 index 00000000..acb25cd7 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java @@ -0,0 +1,34 @@ +/* + * 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.mutation.mutator.proto; + +import com.code_intelligence.jazzer.mutation.api.ChainedMutatorFactory; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; + +public final class ProtoMutators { + private ProtoMutators() {} + + public static MutatorFactory newFactory() { + try { + Class.forName("com.google.protobuf.Message"); + return new ChainedMutatorFactory( + new ByteStringMutatorFactory(), new MessageMutatorFactory(), new BuilderMutatorFactory()); + } catch (ClassNotFoundException e) { + return new ChainedMutatorFactory(); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/TypeLibrary.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/TypeLibrary.java new file mode 100644 index 00000000..43338f14 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/TypeLibrary.java @@ -0,0 +1,179 @@ +/* + * 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.mutation.mutator.proto; + +import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.withoutInit; +import static com.code_intelligence.jazzer.mutation.support.Preconditions.check; +import static com.code_intelligence.jazzer.mutation.support.StreamSupport.entry; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asAnnotatedType; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.containedInDirectedCycle; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.notNull; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.withTypeArguments; +import static java.lang.String.format; +import static java.util.Collections.unmodifiableMap; +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toMap; + +import com.code_intelligence.jazzer.mutation.annotation.NotNull; +import com.code_intelligence.jazzer.mutation.annotation.proto.WithDefaultInstance; +import com.code_intelligence.jazzer.mutation.api.InPlaceMutator; +import com.code_intelligence.jazzer.mutation.support.TypeHolder; +import com.google.protobuf.ByteString; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.EnumValueDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.Message; +import com.google.protobuf.Message.Builder; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Stream; + +final class TypeLibrary { + private static final AnnotatedType RAW_LIST = new TypeHolder<@NotNull List>() {}.annotatedType(); + private static final AnnotatedType RAW_MAP = new TypeHolder<@NotNull Map>() {}.annotatedType(); + private static final Map<JavaType, AnnotatedType> BASE_TYPE_WITH_PRESENCE = + Stream + .of(entry(JavaType.BOOLEAN, Boolean.class), entry(JavaType.BYTE_STRING, ByteString.class), + entry(JavaType.DOUBLE, Double.class), entry(JavaType.ENUM, EnumValueDescriptor.class), + entry(JavaType.FLOAT, Float.class), entry(JavaType.INT, Integer.class), + entry(JavaType.LONG, Long.class), entry(JavaType.MESSAGE, Message.class), + entry(JavaType.STRING, String.class)) + .collect(collectingAndThen(toMap(Entry::getKey, e -> asAnnotatedType(e.getValue())), + map -> unmodifiableMap(new EnumMap<>(map)))); + + private TypeLibrary() {} + + static <T extends Builder> AnnotatedType getTypeToMutate(FieldDescriptor field) { + if (field.isRequired()) { + return getBaseType(field); + } else if (field.isMapField()) { + // Map fields are represented as repeated message fields, so this check has to come before the + // one for regular repeated fields. + AnnotatedType keyType = getBaseType(field.getMessageType().getFields().get(0)); + AnnotatedType valueType = getBaseType(field.getMessageType().getFields().get(1)); + return withTypeArguments(RAW_MAP, keyType, valueType); + } else if (field.isRepeated()) { + return withTypeArguments(RAW_LIST, getBaseType(field)); + } else if (field.hasPresence()) { + return BASE_TYPE_WITH_PRESENCE.get(field.getJavaType()); + } else { + return getBaseType(field); + } + } + + private static <T extends Builder> AnnotatedType getBaseType(FieldDescriptor field) { + return notNull(BASE_TYPE_WITH_PRESENCE.get(field.getJavaType())); + } + + static <T> InPlaceMutator<T> withoutInitIfRecursive( + InPlaceMutator<T> mutator, FieldDescriptor field) { + if (field.isRequired() || !isRecursiveField(field)) { + return mutator; + } + return withoutInit(mutator); + } + + private static boolean isRecursiveField(FieldDescriptor field) { + return containedInDirectedCycle(field, f -> { + // For map fields, only the value can be a message. + FieldDescriptor realField = f.isMapField() ? f.getMessageType().getFields().get(1) : f; + if (realField.getJavaType() != JavaType.MESSAGE) { + return Stream.empty(); + } + return realField.getMessageType().getFields().stream(); + }); + } + + static Message getDefaultInstance(Class<? extends Message> messageClass) { + Method getDefaultInstance; + try { + getDefaultInstance = messageClass.getMethod("getDefaultInstance"); + check(Modifier.isStatic(getDefaultInstance.getModifiers())); + } catch (NoSuchMethodException e) { + throw new IllegalStateException( + format("Message class for builder type %s does not have a getDefaultInstance method", + messageClass.getName()), + e); + } + try { + return (Message) getDefaultInstance.invoke(null); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + format(getDefaultInstance + " isn't accessible or threw an exception"), e); + } + } + + static Message getDefaultInstance(WithDefaultInstance withDefaultInstance) { + String[] parts = withDefaultInstance.value().split("#"); + if (parts.length != 2) { + throw new IllegalArgumentException( + format("Expected @WithDefaultInstance(\"%s\") to specify a fully-qualified method name" + + " (e.g. com.example.MyClass#getDefaultInstance)", + withDefaultInstance.value())); + } + + Class<?> clazz; + try { + clazz = Class.forName(parts[0]); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException( + format("Failed to find class '%s' specified by @WithDefaultInstance(\"%s\")", parts[0], + withDefaultInstance.value()), + e); + } + + Method method; + try { + method = clazz.getDeclaredMethod(parts[1]); + method.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException( + format("Failed to find method specified by @WithDefaultInstance(\"%s\")", + withDefaultInstance.value()), + e); + } + if (!Modifier.isStatic(method.getModifiers())) { + throw new IllegalArgumentException( + format("Expected method specified by @WithDefaultInstance(\"%s\") to be static", + withDefaultInstance.value())); + } + if (!Message.class.isAssignableFrom(method.getReturnType())) { + throw new IllegalArgumentException(format( + "Expected return type of method specified by @WithDefaultInstance(\"%s\") to be a" + + " subtype of %s, got %s", + withDefaultInstance.value(), Message.class.getName(), method.getReturnType().getName())); + } + + try { + return (Message) method.invoke(null); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalArgumentException( + format("Failed to execute method specified by @WithDefaultInstance(\"%s\")", + withDefaultInstance.value()), + e); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel new file mode 100644 index 00000000..5765ceed --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel @@ -0,0 +1,11 @@ +java_library( + name = "support", + srcs = glob(["*.java"]), + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/mutation:__subpackages__", + "//src/test/java/com/code_intelligence/jazzer/mutation:__subpackages__", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/ExceptionSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/ExceptionSupport.java new file mode 100644 index 00000000..bd5f434b --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/ExceptionSupport.java @@ -0,0 +1,31 @@ +/* + * 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.mutation.support; + +public final class ExceptionSupport { + /** + * Allows throwing any {@link Throwable} unchanged as if it were an unchecked exception. + * + * <p>Example: {@code throw asUnchecked(new IOException())} + */ + @SuppressWarnings("unchecked") + public static <T extends Throwable> T asUnchecked(Throwable t) throws T { + throw(T) t; + } + + private ExceptionSupport() {} +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/InputStreamSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/InputStreamSupport.java new file mode 100644 index 00000000..c643fea2 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/InputStreamSupport.java @@ -0,0 +1,256 @@ +/* + * 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.mutation.support; + +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.util.Objects.requireNonNull; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Queue; + +public final class InputStreamSupport { + public static byte[] readAllBytes(InputStream stream) throws IOException { + requireNonNull(stream); + Queue<byte[]> buffers = new ArrayDeque<>(); + int arrayLength = 0; + outer: + while (true) { + byte[] buffer = new byte[max(8192, stream.available())]; + buffers.add(buffer); + int off = 0; + while (off < buffer.length) { + int bytesRead = stream.read(buffer, off, buffer.length - off); + if (bytesRead == -1) { + break outer; + } + off += bytesRead; + arrayLength += bytesRead; + } + } + + byte[] result = new byte[arrayLength]; + int offset = 0; + byte[] buffer; + int remaining = arrayLength; + while ((buffer = buffers.poll()) != null) { + int toCopy = min(buffer.length, remaining); + System.arraycopy(buffer, 0, result, offset, toCopy); + remaining -= toCopy; + } + return result; + } + + private static final InputStream infiniteZerosStream = new ExtendWithNullInputStream(); + + /** + * @return an infinite stream consisting of 0s + */ + public static InputStream infiniteZeros() { + return infiniteZerosStream; + } + + /** + * @return {@code stream} extended with 0s to an infinite stream + */ + public static InputStream extendWithZeros(InputStream stream) { + if (stream instanceof ExtendWithNullInputStream) { + return stream; + } + return new ExtendWithNullInputStream(requireNonNull(stream)); + } + + public static final class ExtendWithNullInputStream extends InputStream { + private static final InputStream ALWAYS_EOF = new ByteArrayInputStream(new byte[0]); + private final InputStream stream; + private boolean eof; + + private ExtendWithNullInputStream() { + this.stream = ALWAYS_EOF; + this.eof = true; + } + + private ExtendWithNullInputStream(InputStream stream) { + this.stream = stream; + this.eof = false; + } + + @Override + public int read() throws IOException { + if (eof) { + return 0; + } + + int res = stream.read(); + if (res != -1) { + return res; + } else { + eof = true; + return 0; + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (eof) { + Arrays.fill(b, off, off + len, (byte) 0); + } else { + int bytesRead = stream.read(b, off, len); + if (bytesRead < len) { + eof = true; + Arrays.fill(b, max(off, off + bytesRead), off + len, (byte) 0); + } + } + return len; + } + + @Override + public int available() throws IOException { + if (eof) { + return Integer.MAX_VALUE; + } else { + return stream.available(); + } + } + + @Override + public void close() throws IOException { + stream.close(); + } + } + + /** + * @return a stream with the first {@code bytes} bytes of {@code stream} + */ + public static InputStream cap(InputStream stream, long bytes) { + requireNonNull(stream); + require(bytes >= 0, "bytes must be non-negative"); + return new CappedInputStream(stream, bytes); + } + + private static final class CappedInputStream extends InputStream { + private final InputStream stream; + private long remaining; + + CappedInputStream(InputStream stream, long remaining) { + this.stream = stream; + this.remaining = remaining; + } + + @Override + public int read() throws IOException { + if (remaining == 0) { + return -1; + } + + int res = stream.read(); + if (res != -1) { + --remaining; + } + return res; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (remaining == 0) { + return -1; + } + + int res = stream.read(b, off, (int) min(len, remaining)); + if (res != -1) { + remaining -= res; + } + return res; + } + + @Override + public int available() throws IOException { + return (int) min(stream.available(), remaining); + } + + @Override + public void close() throws IOException { + stream.close(); + } + } + + /** + * Wraps a given stream with the functionality to detect if it was read exactly. + * To do so, the stream must provide an accurate implementation of {@link + * InputStream#available()}, hence it's restricted to {@link ByteArrayInputStream} for now. + * + * @return {@code stream} extended that detects if it was consumed exactly + */ + public static ReadExactlyInputStream extendWithReadExactly(ByteArrayInputStream stream) { + return new ReadExactlyInputStream(requireNonNull(stream)); + } + + public static final class ReadExactlyInputStream extends InputStream { + private final InputStream stream; + private boolean eof; + + private ReadExactlyInputStream(InputStream stream) { + this.stream = stream; + this.eof = false; + } + + public boolean isConsumedExactly() { + try { + // Forwards availability check to the underlying ByteInputStream, + // which is accurate for the number of available bytes. + return !eof && available() == 0; + } catch (IOException e) { + return false; + } + } + + @Override + public int read() throws IOException { + int res = stream.read(); + if (res == -1) { + eof = true; + } + return res; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int read = stream.read(b, off, len); + if (read < len) { + eof = true; + } + return read; + } + + @Override + public int available() throws IOException { + return stream.available(); + } + + @Override + public void close() throws IOException { + stream.close(); + } + } + + private InputStreamSupport() {} +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/ParameterHolder.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/ParameterHolder.java new file mode 100644 index 00000000..316a6dfd --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/ParameterHolder.java @@ -0,0 +1,63 @@ +/* + * 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.mutation.support; + +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static java.util.stream.Collectors.toList; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; + +/** + * A factory for {@link AnnotatedType} instances capturing method parameters. + * + * <p>Due to type erasure, this class can only be used by creating an anonymous subclass with a + * method called {@code foo} that takes exactly the desired parameter. + * + * <p>Example: {@code new ParameterHolder {void foo(@NotNull List<String> param)}.annotatedType} + */ +public abstract class ParameterHolder { + protected ParameterHolder() {} + + public AnnotatedType annotatedType() { + return getMethod().getAnnotatedParameterTypes()[0]; + } + + public Type type() { + return annotatedType().getType(); + } + + public Annotation[] parameterAnnotations() { + return getMethod().getParameterAnnotations()[0]; + } + + private Method getMethod() { + List<Method> foos = Arrays.stream(this.getClass().getDeclaredMethods()) + .filter(method -> method.getName().equals("foo")) + .collect(toList()); + require(foos.size() == 1, + this.getClass().getName() + " must define exactly one function named 'foo'"); + Method foo = foos.get(0); + require(foo.getParameterCount() == 1, + this.getClass().getName() + "#foo must define exactly one parameter"); + return foo; + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/Preconditions.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/Preconditions.java new file mode 100644 index 00000000..a77f65fb --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/Preconditions.java @@ -0,0 +1,55 @@ +/* + * 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.mutation.support; + +import static java.util.Objects.requireNonNull; + +public final class Preconditions { + private Preconditions() {} + + public static void check(boolean property) { + if (!property) { + throw new IllegalStateException(); + } + } + + public static void check(boolean property, String message) { + if (!property) { + throw new IllegalStateException(message); + } + } + + public static void require(boolean property) { + if (!property) { + throw new IllegalArgumentException(); + } + } + + public static void require(boolean property, String message) { + if (!property) { + throw new IllegalArgumentException(message); + } + } + + public static <T> T[] requireNonNullElements(T[] array) { + requireNonNull(array); + for (T element : array) { + requireNonNull(element, "array must not contain null elements"); + } + return array; + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/RandomSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/RandomSupport.java new file mode 100644 index 00000000..5cfa765e --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/RandomSupport.java @@ -0,0 +1,64 @@ +/* + * 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.mutation.support; + +import java.util.SplittableRandom; + +public final class RandomSupport { + private RandomSupport() {} + + /** + * Polyfill for {@link SplittableRandom#nextBytes(byte[])}, which is not available in Java 8. + */ + public static void nextBytes(SplittableRandom random, byte[] bytes) { + // Taken from the implementation contract + // https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/random/RandomGenerator.html#nextBytes(byte%5B%5D) + // for interoperability with the RandomGenerator interface available as of Java 17. + int i = 0; + int len = bytes.length; + for (int words = len >> 3; words-- > 0;) { + long rnd = random.nextLong(); + for (int n = 8; n-- > 0; rnd >>>= Byte.SIZE) bytes[i++] = (byte) rnd; + } + if (i < len) + for (long rnd = random.nextLong(); i<len; rnd>>>= Byte.SIZE) bytes[i++] = (byte) rnd; + } + + /** + * Clamp function for integers, which Java does not yet have + * + * @param value the value you want to clamp + * @param min the minimum allowable value (inclusive) + * @param max the maximum allowable value (inclusive) + * @return Closest number to {@code value} within the range {@code [min, max]} + */ + public static int clamp(int value, int min, int max) { + return Math.min(Math.max(value, min), max); + } + + /** + * Clamp function for longs, which Java does not yet have + * + * @param value the value you want to clamp + * @param min the minimum allowable value (inclusive) + * @param max the maximum allowable value (inclusive) + * @return Closest number to {@code value} within the range {@code [min, max]} + */ + public static long clamp(long value, long min, long max) { + return Math.min(Math.max(value, min), max); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/StreamSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/StreamSupport.java new file mode 100644 index 00000000..b61980c4 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/StreamSupport.java @@ -0,0 +1,72 @@ +/* + * 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.mutation.support; + +import static java.util.stream.Collectors.toList; + +import java.util.AbstractMap.SimpleEntry; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.function.IntFunction; +import java.util.stream.Stream; + +public final class StreamSupport { + private StreamSupport() {} + + public static boolean[] toBooleanArray(Stream<Boolean> stream) { + List<Boolean> list = stream.collect(toList()); + boolean[] array = new boolean[list.size()]; + for (int i = 0; i < list.size(); i++) { + array[i] = list.get(i); + } + return array; + } + + /** + * @return the first present value, otherwise {@link Optional#empty()} + */ + public static <T> Optional<T> findFirstPresent(Stream<Optional<T>> stream) { + return stream.filter(Optional::isPresent).map(Optional::get).findFirst(); + } + + /** + * @return an array with the values if all {@link Optional}s are present, otherwise + * {@link Optional#empty()} + */ + public static <T> Optional<T[]> toArrayOrEmpty( + Stream<Optional<T>> stream, IntFunction<T[]> newArray) { + try { + return Optional.of(stream.map(Optional::get).toArray(newArray)); + } catch (NoSuchElementException e) { + return Optional.empty(); + } + } + + /** + * Return a stream containing the optional value if present, otherwise an empty stream. + * + * @return stream containing the optional value + */ + public static <T> Stream<T> getOrEmpty(Optional<T> optional) { + return optional.isPresent() ? Stream.of(optional.get()) : Stream.empty(); + } + + public static <K, V> SimpleEntry<K, V> entry(K key, V value) { + return new SimpleEntry<>(key, value); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeHolder.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeHolder.java new file mode 100644 index 00000000..e703f9f1 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeHolder.java @@ -0,0 +1,43 @@ +/* + * 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.mutation.support; + +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Type; + +/** + * A factory for {@link AnnotatedType} instances capturing types. + * + * <p>Due to type erasure, this class can only be used by creating an anonymous subclass. + * + * <p>Example: {@code new TypeHolder<List<String>> {}.annotatedType} + * + * @param <T> the type to hold + */ +public abstract class TypeHolder<T> { + protected TypeHolder() {} + + public AnnotatedType annotatedType() { + return ((AnnotatedParameterizedType) this.getClass().getAnnotatedSuperclass()) + .getAnnotatedActualTypeArguments()[0]; + } + + public Type type() { + return annotatedType().getType(); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java new file mode 100644 index 00000000..8ce3aefc --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java @@ -0,0 +1,564 @@ +/* + * 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.mutation.support; + +import static com.code_intelligence.jazzer.mutation.support.Preconditions.check; +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static com.code_intelligence.jazzer.mutation.support.Preconditions.requireNonNullElements; +import static java.util.Arrays.stream; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; + +import com.code_intelligence.jazzer.mutation.annotation.NotNull; +import com.code_intelligence.jazzer.mutation.annotation.WithLength; +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.reflect.AnnotatedArrayType; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.AnnotatedTypeVariable; +import java.lang.reflect.AnnotatedWildcardType; +import java.lang.reflect.Array; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public final class TypeSupport { + private static final Annotation NOT_NULL = + new TypeHolder<@NotNull String>() {}.annotatedType().getAnnotation(NotNull.class); + + private TypeSupport() {} + + public static boolean isPrimitive(AnnotatedType type) { + return isPrimitive(type.getType()); + } + + public static boolean isPrimitive(Type type) { + if (!(type instanceof Class<?>) ) { + return false; + } + return ((Class<?>) type).isPrimitive(); + } + + public static boolean isInheritable(Annotation annotation) { + return annotation.annotationType().getDeclaredAnnotation(Inherited.class) != null; + } + + /** + * Returns {@code type} as a {@code Class<? extends T>} if it is a subclass of T, otherwise + * empty. + * + * <p>This function also returns an empty {@link Optional} for more complex (e.g. parameterized) + * types. + */ + public static <T> Optional<Class<? extends T>> asSubclassOrEmpty( + AnnotatedType type, Class<T> superclass) { + if (!(type.getType() instanceof Class<?>) ) { + return Optional.empty(); + } + + Class<?> actualClazz = (Class<?>) type.getType(); + if (!superclass.isAssignableFrom(actualClazz)) { + return Optional.empty(); + } + + return Optional.of(actualClazz.asSubclass(superclass)); + } + + public static AnnotatedType asAnnotatedType(Class<?> clazz) { + requireNonNull(clazz); + return new AnnotatedType() { + @Override + public Type getType() { + return clazz; + } + + @Override + public <T extends Annotation> T getAnnotation(Class<T> annotationClass) { + return annotatedElementGetAnnotation(this, annotationClass); + } + + @Override + public Annotation[] getAnnotations() { + // No directly present annotations, look for inheritable present annotations on the + // superclass. + if (clazz.getSuperclass() == null) { + return new Annotation[0]; + } + return stream(clazz.getSuperclass().getAnnotations()) + .filter(TypeSupport::isInheritable) + .toArray(Annotation[] ::new); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + // No directly present annotations. + return new Annotation[0]; + } + + @Override + public String toString() { + return annotatedTypeToString(this); + } + + @Override + public int hashCode() { + throw new UnsupportedOperationException( + "hashCode() is not supported as its behavior isn't specified"); + } + + @Override + public boolean equals(Object obj) { + throw new UnsupportedOperationException( + "equals() is not supported as its behavior isn't specified"); + } + }; + } + + /** + * Visits the individual classes and their directly present annotations that make up the given + * type. + * + * <p>Classes are visited in left-to-right order as they appear in the type definition, except + * that an array class is visited before its component class. + * + * @throws IllegalArgumentException if the given type contains a wildcard type or type variable + */ + public static void visitAnnotatedType( + AnnotatedType type, BiConsumer<Class<?>, Annotation[]> visitor) { + visitAnnotatedTypeInternal(type, visitor); + } + + private static Class<?> visitAnnotatedTypeInternal( + AnnotatedType type, BiConsumer<Class<?>, Annotation[]> visitor) { + Class<?> clazz; + if (type instanceof AnnotatedWildcardType) { + throw new IllegalArgumentException("Wildcard types are not supported: " + type); + } else if (type instanceof AnnotatedTypeVariable) { + throw new IllegalArgumentException("Type variables are not supported: " + type); + } else if (type instanceof AnnotatedParameterizedType) { + AnnotatedParameterizedType annotatedParameterizedType = (AnnotatedParameterizedType) type; + check(annotatedParameterizedType.getType() instanceof ParameterizedType); + Type rawType = ((ParameterizedType) annotatedParameterizedType.getType()).getRawType(); + check(rawType instanceof Class<?>); + clazz = (Class<?>) rawType; + + visitor.accept(clazz, type.getDeclaredAnnotations()); + for (AnnotatedType typeArgument : + annotatedParameterizedType.getAnnotatedActualTypeArguments()) { + visitAnnotatedTypeInternal(typeArgument, visitor); + } + } else if (type instanceof AnnotatedArrayType) { + AnnotatedArrayType arrayType = (AnnotatedArrayType) type; + + // Recursively determine the array class before visiting the component type. + Class<?> componentClass = + visitAnnotatedTypeInternal(arrayType.getAnnotatedGenericComponentType(), (c, a) -> {}); + clazz = Array.newInstance(componentClass, 0).getClass(); + visitor.accept(clazz, type.getDeclaredAnnotations()); + visitAnnotatedTypeInternal(arrayType.getAnnotatedGenericComponentType(), visitor); + } else { + check(type.getType() instanceof Class<?>); + clazz = (Class<?>) type.getType(); + + visitor.accept(clazz, type.getDeclaredAnnotations()); + } + return clazz; + } + + public static AnnotatedType notNull(AnnotatedType type) { + return withExtraAnnotations(type, NOT_NULL); + } + + /** + * Constructs an anonymous WithLength class that can be applied as an annotation to {@code type} + * with the given + * {@code min} and {@code max} values. + * @param type + * @param min + * @param max + * @return {@code type} with a `WithLength` annotation applied to it + */ + public static AnnotatedType withLength(AnnotatedType type, int min, int max) { + WithLength withLength = withLengthImplementation(min, max); + return withExtraAnnotations(type, withLength); + } + + private static WithLength withLengthImplementation(int min, int max) { + return new WithLength() { + @Override + public int min() { + return min; + } + + @Override + public int max() { + return max; + } + + @Override + public Class<? extends Annotation> annotationType() { + return WithLength.class; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof WithLength)) { + return false; + } + WithLength other = (WithLength) o; + return this.min() == other.min() && this.max() == other.max(); + } + + @Override + public int hashCode() { + int hash = 0; + hash += ("min".hashCode() * 127) ^ Integer.valueOf(this.min()).hashCode(); + hash += ("max".hashCode() * 127) ^ Integer.valueOf(this.max()).hashCode(); + return hash; + } + }; + } + + public static AnnotatedParameterizedType withTypeArguments( + AnnotatedType type, AnnotatedType... typeArguments) { + requireNonNull(type); + requireNonNullElements(typeArguments); + require(typeArguments.length > 0); + require(!(type instanceof AnnotatedParameterizedType || type instanceof AnnotatedTypeVariable + || type instanceof AnnotatedWildcardType || type instanceof AnnotatedArrayType), + "only plain annotated types are supported"); + require( + ((Class<?>) type.getType()).getEnclosingClass() == null, "nested classes aren't supported"); + + ParameterizedType filledRawType = new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return stream(typeArguments).map(AnnotatedType::getType).toArray(Type[] ::new); + } + + @Override + public Type getRawType() { + return type.getType(); + } + + @Override + public Type getOwnerType() { + // We require the class is top-level. + return null; + } + + @Override + public String toString() { + return getRawType() + + stream(getActualTypeArguments()).map(Type::toString).collect(joining(",", "<", ">")); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ParameterizedType)) { + return false; + } + ParameterizedType other = (ParameterizedType) obj; + return getRawType().equals(other.getRawType()) && null == other.getOwnerType() + && Arrays.equals(getActualTypeArguments(), other.getActualTypeArguments()); + } + + @Override + public int hashCode() { + throw new UnsupportedOperationException( + "hashCode() is not supported as its behavior isn't specified"); + } + }; + + return new AnnotatedParameterizedType() { + @Override + public AnnotatedType[] getAnnotatedActualTypeArguments() { + return Arrays.copyOf(typeArguments, typeArguments.length); + } + + // @Override as of Java 9 + @SuppressWarnings("Since15") + public AnnotatedType getAnnotatedOwnerType() { + return null; + } + + @Override + public Type getType() { + return filledRawType; + } + + @Override + public <T extends Annotation> T getAnnotation(Class<T> annotationClass) { + return type.getAnnotation(annotationClass); + } + + @Override + public Annotation[] getAnnotations() { + return type.getAnnotations(); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + return type.getDeclaredAnnotations(); + } + + @Override + public String toString() { + return annotatedTypeToString(this); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AnnotatedParameterizedType)) { + return false; + } + AnnotatedParameterizedType other = (AnnotatedParameterizedType) obj; + // Can't call getAnnotatedOwnerType on Java 8, but since our own implementation always + // returns null, comparing getType().getOwnerType() via getType() is sufficient. + return Objects.equals(getType(), other.getType()) + && Arrays.equals( + getAnnotatedActualTypeArguments(), other.getAnnotatedActualTypeArguments()) + && Arrays.equals(getAnnotations(), other.getAnnotations()); + } + + @Override + public int hashCode() { + throw new UnsupportedOperationException( + "hashCode() is not supported as its behavior isn't specified"); + } + }; + } + + public static AnnotatedType withExtraAnnotations( + AnnotatedType base, Annotation... extraAnnotations) { + requireNonNull(base); + requireNonNullElements(extraAnnotations); + + if (extraAnnotations.length == 0) { + return base; + } + + require(!(base instanceof AnnotatedTypeVariable || base instanceof AnnotatedWildcardType), + "Adding annotations to AnnotatedTypeVariables or AnnotatedWildcardTypes is not supported"); + if (base instanceof AnnotatedArrayType) { + return new AugmentedArrayType((AnnotatedArrayType) base, extraAnnotations); + } else if (base instanceof AnnotatedParameterizedType) { + return new AugmentedParameterizedType((AnnotatedParameterizedType) base, extraAnnotations); + } else { + return new AugmentedAnnotatedType(base, extraAnnotations); + } + } + + private static String annotatedTypeToString(AnnotatedType annotatedType) { + String annotations = + stream(annotatedType.getAnnotations()).map(Annotation::toString).collect(joining(" ")); + if (annotations.isEmpty()) { + return annotatedType.getType().toString(); + } else { + return annotations + " " + annotatedType.getType(); + } + } + + private static <T extends Annotation> T annotatedElementGetAnnotation( + AnnotatedElement element, Class<T> annotationClass) { + requireNonNull(annotationClass); + return stream(element.getAnnotations()) + .filter(annotation -> annotationClass.equals(annotation.annotationType())) + .findFirst() + .map(annotationClass::cast) + .orElse(null); + } + + public static Optional<Class<?>> findFirstParentIfClass(AnnotatedType type, Class<?>... parents) { + if (!(type.getType() instanceof Class<?>) ) { + return Optional.empty(); + } + Class<?> clazz = (Class<?>) type.getType(); + return Stream.of(parents).filter(parent -> parent.isAssignableFrom(clazz)).findFirst(); + } + + public static Optional<AnnotatedType> parameterTypeIfParameterized( + AnnotatedType type, Class<?> expectedParent) { + return parameterTypesIfParameterized(type, expectedParent).flatMap(typeArguments -> { + if (typeArguments.size() != 1) { + return Optional.empty(); + } else { + AnnotatedType elementType = typeArguments.get(0); + if (!(elementType.getType() instanceof ParameterizedType) + && !(elementType.getType() instanceof Class)) { + return Optional.empty(); + } else { + return Optional.of(elementType); + } + } + }); + } + + public static Optional<List<AnnotatedType>> parameterTypesIfParameterized( + AnnotatedType type, Class<?> expectedParent) { + if (!(type instanceof AnnotatedParameterizedType)) { + return Optional.empty(); + } + Class<?> clazz = (Class<?>) ((ParameterizedType) type.getType()).getRawType(); + if (!expectedParent.isAssignableFrom(clazz)) { + return Optional.empty(); + } + + AnnotatedType[] typeArguments = + ((AnnotatedParameterizedType) type).getAnnotatedActualTypeArguments(); + if (typeArguments.length == 0) { + return Optional.empty(); + } + return Optional.of(Collections.unmodifiableList(Arrays.asList(typeArguments))); + } + + /** + * Whether {@code root} is contained in a directed cycle in the directed graph rooted at it and + * defined by the given {@code successors} function. + */ + public static <T> boolean containedInDirectedCycle(T root, Function<T, Stream<T>> successors) { + HashSet<T> traversed = new HashSet<>(); + ArrayDeque<T> toTraverse = new ArrayDeque<>(); + toTraverse.addLast(root); + T currentNode; + while ((currentNode = toTraverse.pollLast()) != null) { + if (traversed.add(currentNode)) { + successors.apply(currentNode).forEachOrdered(toTraverse::addLast); + } else if (currentNode.equals(root)) { + return true; + } + } + return false; + } + + private static class AugmentedArrayType + extends AugmentedAnnotatedType implements AnnotatedArrayType { + private AugmentedArrayType(AnnotatedArrayType base, Annotation[] extraAnnotations) { + super(base, extraAnnotations); + } + + @Override + public AnnotatedType getAnnotatedGenericComponentType() { + return ((AnnotatedArrayType) base).getAnnotatedGenericComponentType(); + } + + // @Override as of Java 9 + @SuppressWarnings("Since15") + public AnnotatedType getAnnotatedOwnerType() { + throw new UnsupportedOperationException("Not implemented"); + } + } + + private static class AugmentedParameterizedType + extends AugmentedAnnotatedType implements AnnotatedParameterizedType { + private AugmentedParameterizedType( + AnnotatedParameterizedType base, Annotation[] extraAnnotations) { + super(base, extraAnnotations); + } + + @Override + public AnnotatedType[] getAnnotatedActualTypeArguments() { + return ((AnnotatedParameterizedType) base).getAnnotatedActualTypeArguments(); + } + + // @Override as of Java 9 + @SuppressWarnings("Since15") + public AnnotatedType getAnnotatedOwnerType() { + throw new UnsupportedOperationException("Not implemented"); + } + } + + private static class AugmentedAnnotatedType implements AnnotatedType { + protected final AnnotatedType base; + private final Annotation[] extraAnnotations; + + private AugmentedAnnotatedType(AnnotatedType base, Annotation[] extraAnnotations) { + this.base = requireNonNull(base); + this.extraAnnotations = checkExtraAnnotations(base, extraAnnotations); + } + + private static Annotation[] checkExtraAnnotations( + AnnotatedElement base, Annotation[] extraAnnotations) { + requireNonNullElements(extraAnnotations); + Set<Class<? extends Annotation>> existingAnnotationTypes = + stream(base.getDeclaredAnnotations()) + .map(Annotation::annotationType) + .collect(Collectors.toCollection(HashSet::new)); + for (Annotation annotation : extraAnnotations) { + boolean added = existingAnnotationTypes.add(annotation.annotationType()); + require(added, annotation + " already directly present on " + base); + } + return extraAnnotations; + } + + @Override + public Type getType() { + return base.getType(); + } + + @Override + public <T extends Annotation> T getAnnotation(Class<T> annotationClass) { + return annotatedElementGetAnnotation(this, annotationClass); + } + + @Override + public Annotation[] getAnnotations() { + Set<Class<? extends Annotation>> directlyPresentTypes = + stream(getDeclaredAnnotations()).map(Annotation::annotationType).collect(toSet()); + return Stream + .concat( + // Directly present annotations. + stream(getDeclaredAnnotations()), + // Present but not directly present annotations, never added by us as we don't add + // annotations to the super class. + stream(base.getAnnotations()) + .filter( + annotation -> !directlyPresentTypes.contains(annotation.annotationType()))) + .toArray(Annotation[] ::new); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + return Stream.concat(stream(base.getDeclaredAnnotations()), stream(extraAnnotations)) + .toArray(Annotation[] ::new); + } + + @Override + public String toString() { + return annotatedTypeToString(this); + } + + @Override + public boolean equals(Object obj) { + throw new UnsupportedOperationException( + "equals() is not supported as its behavior isn't specified"); + } + + @Override + public int hashCode() { + throw new UnsupportedOperationException( + "hashCode() is not supported as its behavior isn't specified"); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/WeakIdentityHashMap.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/WeakIdentityHashMap.java new file mode 100644 index 00000000..0eef7c24 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/WeakIdentityHashMap.java @@ -0,0 +1,168 @@ +/* + * 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.mutation.support; + +import static java.util.stream.Collectors.toSet; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.AbstractMap.SimpleEntry; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * An unoptimized version of a {@link java.util.WeakHashMap} with the semantics of a + * {@link java.util.IdentityHashMap}. + * + * <p>If this class ever becomes a bottleneck, e.g. because of the IdentityWeakReference + * allocations, it should be replaced by a copy of the * {@link java.util.WeakHashMap} code with all + * {@code equals} calls dropped and all {@code hashCode} * calls replaced with {@link + * System#identityHashCode}. + */ +public final class WeakIdentityHashMap<K, V> implements Map<K, V> { + private final HashMap<WeakReference<K>, V> map = new HashMap<>(); + private final ReferenceQueue<K> weaklyReachables = new ReferenceQueue<>(); + + @Override + public int size() { + removeNewWeaklyReachables(); + return map.size(); + } + + @Override + public boolean isEmpty() { + removeNewWeaklyReachables(); + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + removeNewWeaklyReachables(); + return map.containsKey(new IdentityWeakReference<>(key)); + } + + @Override + public boolean containsValue(Object value) { + removeNewWeaklyReachables(); + return map.containsValue(value); + } + + @Override + public V get(Object key) { + removeNewWeaklyReachables(); + return map.get(new IdentityWeakReference<>(key)); + } + + @Override + public V put(K key, V value) { + removeNewWeaklyReachables(); + return map.put(new IdentityWeakReference<>(key, weaklyReachables), value); + } + + @Override + public V remove(Object key) { + removeNewWeaklyReachables(); + return map.remove(new IdentityWeakReference<>(key)); + } + + @Override + public void putAll(Map<? extends K, ? extends V> otherMap) { + removeNewWeaklyReachables(); + for (Entry<? extends K, ? extends V> entry : otherMap.entrySet()) { + map.put(new IdentityWeakReference<>(entry.getKey(), weaklyReachables), entry.getValue()); + } + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public Set<K> keySet() { + removeNewWeaklyReachables(); + return map.keySet().stream().map(WeakReference::get).filter(Objects::nonNull).collect(toSet()); + } + + @Override + public Collection<V> values() { + removeNewWeaklyReachables(); + return map.values(); + } + + @Override + public Set<Entry<K, V>> entrySet() { + removeNewWeaklyReachables(); + return map.entrySet() + .stream() + .map(e -> new SimpleEntry<>(e.getKey().get(), e.getValue())) + .filter(e -> e.getKey() != null) + .collect(toSet()); + } + + void collectKeysForTesting() { + map.keySet().forEach(ref -> { + ref.clear(); + ref.enqueue(); + }); + } + + private void removeNewWeaklyReachables() { + Reference<? extends K> referent; + while ((referent = weaklyReachables.poll()) != null) { + map.remove(referent); + } + } + + private static final class IdentityWeakReference<T> extends WeakReference<T> { + private final int referentHashCode; + + public IdentityWeakReference(T referent) { + super(referent); + this.referentHashCode = System.identityHashCode(referent); + } + + public IdentityWeakReference(T referent, ReferenceQueue<? super T> queue) { + super(referent, queue); + this.referentHashCode = System.identityHashCode(referent); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof WeakReference)) { + return false; + } + T referent = get(); + if (referent == null) { + return false; + } + return referent == ((WeakReference<?>) other).get(); + } + + @Override + public int hashCode() { + return referentHashCode; + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel new file mode 100644 index 00000000..4b534545 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel @@ -0,0 +1,16 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library") + +java_jni_library( + name = "replay", + srcs = ["Replayer.java"], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/api", + "//src/main/java/com/code_intelligence/jazzer/driver:fuzzed_data_provider_impl", + ], +) + +java_binary( + name = "Replayer", + visibility = ["//visibility:public"], + runtime_deps = [":replay"], +) diff --git a/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java b/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java new file mode 100644 index 00000000..dc76328e --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java @@ -0,0 +1,144 @@ +// Copyright 2021 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.replay; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.driver.FuzzedDataProviderImpl; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; + +public class Replayer { + public static final int STATUS_FINDING = 77; + public static final int STATUS_OTHER_ERROR = 1; + + public static void main(String[] args) { + if (args.length < 2) { + System.err.println("Usage: <fuzz target class> <input file path> <fuzzerInitialize args>..."); + System.exit(STATUS_OTHER_ERROR); + } + ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true); + + Class<?> fuzzTargetClass; + try { + fuzzTargetClass = Class.forName(args[0]); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + System.exit(STATUS_OTHER_ERROR); + // Without this return the compiler sees fuzzTargetClass as possibly uninitialized. + return; + } + + String inputFilePath = args[1]; + byte[] input = loadInput(inputFilePath); + + String[] fuzzTargetArgs = Arrays.copyOfRange(args, 2, args.length); + executeFuzzerInitialize(fuzzTargetClass, fuzzTargetArgs); + executeFuzzTarget(fuzzTargetClass, input); + } + + private static byte[] loadInput(String inputFilePath) { + try { + return Files.readAllBytes(Paths.get(inputFilePath)); + } catch (IOException e) { + e.printStackTrace(); + System.exit(STATUS_OTHER_ERROR); + // Without this return the compiler sees loadInput as possibly not returning a value. + return null; + } + } + + private static void executeFuzzerInitialize(Class<?> fuzzTarget, String[] args) { + // public static void fuzzerInitialize() + try { + Method fuzzerInitialize = fuzzTarget.getMethod("fuzzerInitialize"); + fuzzerInitialize.invoke(null); + return; + } catch (Exception e) { + handleInvokeException(e, fuzzTarget); + } + // public static void fuzzerInitialize(String[] args) + try { + Method fuzzerInitialize = fuzzTarget.getMethod("fuzzerInitialize", String[].class); + fuzzerInitialize.invoke(null, (Object) args); + } catch (Exception e) { + handleInvokeException(e, fuzzTarget); + } + } + + public static void executeFuzzTarget(Class<?> fuzzTarget, byte[] input) { + // public static void fuzzerTestOneInput(byte[] input) + try { + Method fuzzerTestOneInput = fuzzTarget.getMethod("fuzzerTestOneInput", byte[].class); + fuzzerTestOneInput.invoke(null, (Object) input); + return; + } catch (Exception e) { + handleInvokeException(e, fuzzTarget); + } + // public static void fuzzerTestOneInput(FuzzedDataProvider data) + try { + Method fuzzerTestOneInput = + fuzzTarget.getMethod("fuzzerTestOneInput", FuzzedDataProvider.class); + try (FuzzedDataProviderImpl fuzzedDataProvider = FuzzedDataProviderImpl.withJavaData(input)) { + fuzzerTestOneInput.invoke(null, fuzzedDataProvider); + } + return; + } catch (Exception e) { + handleInvokeException(e, fuzzTarget); + } + System.err.printf("%s must define exactly one of the following two functions:%n" + + " public static void fuzzerTestOneInput(byte[] ...)%n" + + " public static void fuzzerTestOneInput(FuzzedDataProvider ...)%n" + + "Note: Fuzz targets returning boolean are no longer supported; exceptions should%n" + + "be thrown instead of returning true.%n", + fuzzTarget.getName()); + System.exit(STATUS_OTHER_ERROR); + } + + private static void handleInvokeException(Exception e, Class<?> fuzzTarget) { + if (e instanceof NoSuchMethodException) + return; + if (e instanceof InvocationTargetException) { + filterOutOwnStackTraceElements(e.getCause(), fuzzTarget); + e.getCause().printStackTrace(); + System.exit(STATUS_FINDING); + } else { + e.printStackTrace(); + System.exit(STATUS_OTHER_ERROR); + } + } + + private static void filterOutOwnStackTraceElements(Throwable t, Class<?> fuzzTarget) { + if (t.getCause() != null) + filterOutOwnStackTraceElements(t.getCause(), fuzzTarget); + if (t.getStackTrace() == null || t.getStackTrace().length == 0) + return; + StackTraceElement lowestFrame = t.getStackTrace()[t.getStackTrace().length - 1]; + if (!lowestFrame.getClassName().equals(Replayer.class.getName()) + || !lowestFrame.getMethodName().equals("main")) + return; + for (int i = t.getStackTrace().length - 1; i >= 0; i--) { + StackTraceElement frame = t.getStackTrace()[i]; + if (frame.getClassName().equals(fuzzTarget.getName()) + && frame.getMethodName().equals("fuzzerTestOneInput")) { + t.setStackTrace(Arrays.copyOfRange(t.getStackTrace(), 0, i + 1)); + break; + } + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel new file mode 100644 index 00000000..c31c86e4 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel @@ -0,0 +1,180 @@ +load("@com_github_johnynek_bazel_jar_jar//:jar_jar.bzl", "jar_jar") +load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library") +load("//bazel:compat.bzl", "SKIP_ON_WINDOWS") +load("//bazel:jar.bzl", "strip_jar") + +# The transitive dependencies of this target will be appended to the search path +# of the bootstrap class loader. They will be visible to all classes - care must +# be taken to shade everything and generally keep this target as small as +# possible. +java_binary( + name = "jazzer_bootstrap_unshaded", + create_executable = False, + runtime_deps = [":jazzer_bootstrap_lib"], +) + +java_library( + name = "jazzer_bootstrap_lib", + visibility = ["//src/main/java/com/code_intelligence/jazzer:__pkg__"], + runtime_deps = [ + ":runtime", + "//sanitizers", + ], +) + +# These classes with public Bazel visibility are contained in jazzer_bootstrap.jar +# and will thus be available on the bootstrap class path. This target can be +# passed to the `deploy_env` attribute of the Jazzer `java_binary` to ensure that +# it doesn't bundle in these classes. +java_binary( + name = "jazzer_bootstrap_env", + create_executable = False, + visibility = ["//src/main/java/com/code_intelligence/jazzer:__pkg__"], + runtime_deps = [ + "//src/main/java/com/code_intelligence/jazzer/api:hooks", + "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider", + ], +) + +jar_jar( + name = "jazzer_bootstrap_unstripped", + input_jar = ":jazzer_bootstrap_unshaded_deploy.jar", + rules = "bootstrap_shade_rules", +) + +strip_jar( + name = "jazzer_bootstrap", + out = "jazzer_bootstrap.jar", + jar = ":jazzer_bootstrap_unstripped", + paths_to_keep = [ + "com/code_intelligence/jazzer/**", + "jaz/**", + "META-INF/MANIFEST.MF", + ], + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/android:__pkg__", + ], +) + +sh_test( + name = "jazzer_bootstrap_shading_test", + srcs = ["verify_shading.sh"], + args = [ + "$(rootpath jazzer_bootstrap.jar)", + ], + data = [ + "jazzer_bootstrap.jar", + "@local_jdk//:bin/jar", + ], + tags = [ + # Coverage instrumentation necessarily adds files to the jar that we + # wouldn't want to release and thus causes this test to fail. + "no-coverage", + ], + target_compatible_with = SKIP_ON_WINDOWS, +) + +# At runtime, the AgentInstaller appends jazzer_bootstrap.jar to the bootstrap +# class loader's search path - these classes must not be available on the +# regular classpath. Since dependents should not have to resort to reflection to +# access these classes they know will be there at runtime, this compile-time +# only dependency can be used as a replacement. +java_library( + name = "jazzer_bootstrap_compile_only", + neverlink = True, + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/autofuzz:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/driver:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/instrumentor:__pkg__", + ], + exports = [ + ":fuzz_target_runner_natives", + ":runtime", + ], +) + +# The following targets must only be referenced directly by tests or native implementations. + +java_jni_library( + name = "coverage_map", + srcs = ["CoverageMap.java"], + native_libs = select({ + "@platforms//os:android": ["//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver"], + "//conditions:default": [], + }), + visibility = [ + "//src/jmh/java/com/code_intelligence/jazzer/instrumentor:__pkg__", + "//src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + "//src/test:__subpackages__", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/runtime:constants", + "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider", + ], +) + +java_jni_library( + name = "trace_data_flow_native_callbacks", + srcs = ["TraceDataFlowNativeCallbacks.java"], + visibility = [ + "//src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + ], + deps = ["@org_ow2_asm_asm//jar"], +) + +java_jni_library( + name = "fuzz_target_runner_natives", + srcs = ["FuzzTargetRunnerNatives.java"], + visibility = ["//src/main/native/com/code_intelligence/jazzer/driver:__pkg__"], + deps = [ + ":constants", + ], +) + +java_jni_library( + name = "mutator", + srcs = ["Mutator.java"], + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer:__pkg__", + "//src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + ], +) + +java_library( + name = "runtime", + srcs = [ + "HardToCatchError.java", + "JazzerInternal.java", + "NativeLibHooks.java", + "TraceCmpHooks.java", + "TraceDivHooks.java", + "TraceIndirHooks.java", + ], + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/android:__pkg__", + "//src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + "//src/test:__subpackages__", + ], + runtime_deps = [ + ":fuzz_target_runner_natives", + ":mutator", + # Access to Unsafe is possible without any tricks if the class that does it is loaded by the + # bootstrap loader. We thus want Jazzer to use this class from jazzer_bootstrap. + "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider", + ], + deps = [ + ":constants", + ":coverage_map", + ":trace_data_flow_native_callbacks", + "//src/main/java/com/code_intelligence/jazzer/api:hooks", + ], +) + +# This target exposes a class that can safely be loaded in both the system and the bootstrap class +# loader as it provides true constants that do not change over the lifetime of the JVM. +java_library( + name = "constants", + srcs = ["Constants.java"], + visibility = ["//visibility:public"], +) diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/Constants.java b/src/main/java/com/code_intelligence/jazzer/runtime/Constants.java new file mode 100644 index 00000000..92f4a3ca --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/Constants.java @@ -0,0 +1,21 @@ +/* + * 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.runtime; + +public final class Constants { + public static final boolean IS_ANDROID = System.getProperty("java.vm.vendor").contains("Android"); +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java b/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java new file mode 100644 index 00000000..a945a30a --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java @@ -0,0 +1,169 @@ +// Copyright 2021 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.runtime; + +import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID; + +import com.code_intelligence.jazzer.utils.UnsafeProvider; +import com.github.fmeum.rules_jni.RulesJni; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import sun.misc.Unsafe; + +/** + * Represents the Java view on a libFuzzer 8 bit counter coverage map. By using a direct ByteBuffer, + * the counters are shared directly with native code. + */ +final public class CoverageMap { + static { + RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver"); + } + + private static final String ENV_MAX_NUM_COUNTERS = "JAZZER_MAX_NUM_COUNTERS"; + + private static final int MAX_NUM_COUNTERS = System.getenv(ENV_MAX_NUM_COUNTERS) != null + ? Integer.parseInt(System.getenv(ENV_MAX_NUM_COUNTERS)) + : 1 << 20; + + private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe(); + private static final Class<?> LOG; + private static final MethodHandle LOG_INFO; + private static final MethodHandle LOG_ERROR; + + static { + try { + LOG = Class.forName( + "com.code_intelligence.jazzer.utils.Log", false, ClassLoader.getSystemClassLoader()); + LOG_INFO = MethodHandles.lookup().findStatic( + LOG, "info", MethodType.methodType(void.class, String.class)); + LOG_ERROR = MethodHandles.lookup().findStatic( + LOG, "error", MethodType.methodType(void.class, String.class, Throwable.class)); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + /** + * The collection of coverage counters directly interacted with by classes that are instrumented + * for coverage. The instrumentation assumes that this is always one contiguous block of memory, + * so it is allocated once at maximum size. Using a larger number here increases the memory usage + * of all fuzz targets, but has otherwise no impact on performance. + */ + public static final long countersAddress = UNSAFE.allocateMemory(MAX_NUM_COUNTERS); + + static { + UNSAFE.setMemory(countersAddress, MAX_NUM_COUNTERS, (byte) 0); + initialize(countersAddress); + } + + private static final int INITIAL_NUM_COUNTERS = 1 << 9; + + static { + registerNewCounters(0, INITIAL_NUM_COUNTERS); + } + + /** + * The number of coverage counters that are currently registered with libFuzzer. This number grows + * dynamically as classes are instrumented and should be kept as low as possible as libFuzzer has + * to iterate over the whole map for every execution. + */ + private static int currentNumCounters = INITIAL_NUM_COUNTERS; + + // Called via reflection. + @SuppressWarnings("unused") + public static void enlargeIfNeeded(int nextId) { + int newNumCounters = currentNumCounters; + while (nextId >= newNumCounters) { + newNumCounters = 2 * newNumCounters; + if (newNumCounters > MAX_NUM_COUNTERS) { + logError( + String.format( + "Maximum number (%s) of coverage counters exceeded. Try to limit the scope of a single fuzz target as " + + "much as possible to keep the fuzzer fast. If that is not possible, the maximum number of " + + "counters can be increased via the %s environment variable.", + MAX_NUM_COUNTERS, ENV_MAX_NUM_COUNTERS), + null); + System.exit(1); + } + } + if (newNumCounters > currentNumCounters) { + registerNewCounters(currentNumCounters, newNumCounters); + currentNumCounters = newNumCounters; + logInfo("New number of coverage counters: " + currentNumCounters); + } + } + + // Called by the coverage instrumentation. + @SuppressWarnings("unused") + public static void recordCoverage(final int id) { + if (IS_ANDROID) { + enlargeIfNeeded(id); + } + + final long address = countersAddress + id; + final byte counter = UNSAFE.getByte(address); + UNSAFE.putByte(address, (byte) (counter == -1 ? 1 : counter + 1)); + } + + public static Set<Integer> getCoveredIds() { + Set<Integer> coveredIds = new HashSet<>(); + for (int id = 0; id < currentNumCounters; id++) { + if (UNSAFE.getByte(countersAddress + id) > 0) { + coveredIds.add(id); + } + } + return Collections.unmodifiableSet(coveredIds); + } + + public static void replayCoveredIds(Set<Integer> coveredIds) { + for (int id : coveredIds) { + UNSAFE.putByte(countersAddress + id, (byte) 1); + } + } + + private static void logInfo(String message) { + try { + LOG_INFO.invokeExact(message); + } catch (Throwable error) { + // Should not be reached, Log.error does not throw. + error.printStackTrace(); + System.err.println("Failed to call Log.info:"); + System.err.println(message); + } + } + + private static void logError(String message, Throwable t) { + try { + LOG_ERROR.invokeExact(message, t); + } catch (Throwable error) { + // Should not be reached, Log.error does not throw. + error.printStackTrace(); + System.err.println("Failed to call Log.error:"); + System.err.println(message); + } + } + + // Returns the IDs of all blocks that have been covered in at least one run (not just the current + // one). + public static native int[] getEverCoveredIds(); + + private static native void initialize(long countersAddress); + + private static native void registerNewCounters(int oldNumCounters, int newNumCounters); +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/FuzzTargetRunnerNatives.java b/src/main/java/com/code_intelligence/jazzer/runtime/FuzzTargetRunnerNatives.java new file mode 100644 index 00000000..bbf74fdb --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/FuzzTargetRunnerNatives.java @@ -0,0 +1,41 @@ +// Copyright 2022 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.runtime; + +import com.github.fmeum.rules_jni.RulesJni; + +/** + * The native functions used by FuzzTargetRunner. + * + * <p>This class has to be loaded by the bootstrap class loader since the native library it loads + * links in libFuzzer and the Java hooks, which have to be on the bootstrap path so that they are + * seen by Java standard library classes, need to be able to call native libFuzzer callbacks. + */ +public class FuzzTargetRunnerNatives { + static { + if (!Constants.IS_ANDROID && FuzzTargetRunnerNatives.class.getClassLoader() != null) { + throw new IllegalStateException( + "FuzzTargetRunnerNatives must be loaded in the bootstrap loader"); + } + RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver"); + } + + public static native int startLibFuzzer( + byte[][] args, Class<?> runner, boolean useExperimentalMutator); + + public static native void printCrashingInput(); + + public static native void temporarilyDisableLibfuzzerExitHook(); +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/HardToCatchError.java b/src/main/java/com/code_intelligence/jazzer/runtime/HardToCatchError.java new file mode 100644 index 00000000..cf136051 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/HardToCatchError.java @@ -0,0 +1,82 @@ +// Copyright 2021 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.runtime; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * An Error that rethrows itself when any of its getters is invoked. + */ +public class HardToCatchError extends Error { + public HardToCatchError() { + super(); + } + + @Override + public String getMessage() { + throw this; + } + + @Override + public String getLocalizedMessage() { + throw this; + } + + @Override + public synchronized Throwable initCause(Throwable cause) { + throw this; + } + + @Override + public String toString() { + throw this; + } + + @Override + public void printStackTrace() { + throw this; + } + + @Override + public void printStackTrace(PrintStream s) { + throw this; + } + + @Override + public void printStackTrace(PrintWriter s) { + throw this; + } + + @Override + public StackTraceElement[] getStackTrace() { + throw this; + } + + @Override + public int hashCode() { + throw this; + } + + @Override + public boolean equals(Object obj) { + throw this; + } + + @Override + public Object clone() { + throw this; + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java b/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java new file mode 100644 index 00000000..3b368531 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java @@ -0,0 +1,50 @@ +// Copyright 2021 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.runtime; + +import java.util.ArrayList; + +final public class JazzerInternal { + public static Throwable lastFinding; + // The value is only relevant when regression testing. Read by the bytecode emitted by + // HookMethodVisitor to enable hooks only when invoked from a @FuzzTest. + // + // Alternatives considered: + // Making this thread local rather than global may potentially allow to run fuzz tests in + // parallel with regular unit tests, but it is next to impossible to determine which thread is + // currently doing work for a fuzz test versus a regular unit test. Instead, @FuzzTest is + // annotated with @Isolated. + @SuppressWarnings("unused") public static boolean hooksEnabled = true; + + private static final ArrayList<Runnable> onFuzzTargetReadyCallbacks = new ArrayList<>(); + + // Accessed from api.Jazzer via reflection. + public static void reportFindingFromHook(Throwable finding) { + lastFinding = finding; + // Throw an Error that is hard to catch (short of outright ignoring it) in order to quickly + // terminate the execution of the fuzz target. The finding will be reported as soon as the fuzz + // target returns even if this Error is swallowed. + throw new HardToCatchError(); + } + + public static void registerOnFuzzTargetReadyCallback(Runnable callback) { + onFuzzTargetReadyCallbacks.add(callback); + } + + public static void onFuzzTargetReady(String fuzzTargetClass) { + onFuzzTargetReadyCallbacks.forEach(Runnable::run); + onFuzzTargetReadyCallbacks.clear(); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/Mutator.java b/src/main/java/com/code_intelligence/jazzer/runtime/Mutator.java new file mode 100644 index 00000000..2d9a7f65 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/Mutator.java @@ -0,0 +1,34 @@ +/* + * 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.runtime; + +import com.github.fmeum.rules_jni.RulesJni; + +public final class Mutator { + public static final boolean SHOULD_MOCK = + Boolean.parseBoolean(System.getenv("JAZZER_MOCK_LIBFUZZER_MUTATOR")); + + static { + if (!SHOULD_MOCK) { + RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver"); + } + } + + public static native int defaultMutateNative(byte[] buffer, int size); + + private Mutator() {} +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/NativeLibHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/NativeLibHooks.java new file mode 100644 index 00000000..8572f05a --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/NativeLibHooks.java @@ -0,0 +1,39 @@ +// Copyright 2021 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.runtime; + +import com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; + +@SuppressWarnings("unused") +final public class NativeLibHooks { + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Runtime", + targetMethod = "loadLibrary", targetMethodDescriptor = "(Ljava/lang/String;)V") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", + targetMethod = "loadLibrary", targetMethodDescriptor = "(Ljava/lang/String;)V") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Runtime", targetMethod = "load", + targetMethodDescriptor = "(Ljava/lang/String;)V") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", targetMethod = "load", + targetMethodDescriptor = "(Ljava/lang/String;)V") + public static void + loadLibraryHook(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (Constants.IS_ANDROID) { + return; + } + + TraceDataFlowNativeCallbacks.handleLibraryLoad(); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java new file mode 100644 index 00000000..7fb15866 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java @@ -0,0 +1,598 @@ +// Copyright 2021 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.runtime; + +import com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; +import java.util.*; + +@SuppressWarnings("unused") +final public class TraceCmpHooks { + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Byte", targetMethod = "compare", + targetMethodDescriptor = "(BB)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Byte", + targetMethod = "compareUnsigned", targetMethodDescriptor = "(BB)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Short", targetMethod = "compare", + targetMethodDescriptor = "(SS)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Short", + targetMethod = "compareUnsigned", targetMethodDescriptor = "(SS)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer", + targetMethod = "compare", targetMethodDescriptor = "(II)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer", + targetMethod = "compareUnsigned", targetMethodDescriptor = "(II)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "kotlin.jvm.internal.Intrinsics ", + targetMethod = "compare", targetMethodDescriptor = "(II)I") + public static void + integerCompare(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId) { + TraceDataFlowNativeCallbacks.traceCmpInt((int) arguments[0], (int) arguments[1], hookId); + } + + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Byte", + targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Byte;)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Short", + targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Short;)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer", + targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Integer;)I") + public static void + integerCompareTo(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + TraceDataFlowNativeCallbacks.traceCmpInt((int) thisObject, (int) arguments[0], hookId); + } + + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long", targetMethod = "compare", + targetMethodDescriptor = "(JJ)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long", + targetMethod = "compareUnsigned", targetMethodDescriptor = "(JJ)I") + public static void + longCompare(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + TraceDataFlowNativeCallbacks.traceCmpLong((long) arguments[0], (long) arguments[1], hookId); + } + + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long", + targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Long;)I") + public static void + longCompareTo(MethodHandle method, Long thisObject, Object[] arguments, int hookId) { + TraceDataFlowNativeCallbacks.traceCmpLong(thisObject, (long) arguments[0], hookId); + } + + @MethodHook(type = HookType.BEFORE, targetClassName = "kotlin.jvm.internal.Intrinsics ", + targetMethod = "compare", targetMethodDescriptor = "(JJ)I") + public static void + longCompareKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId) { + TraceDataFlowNativeCallbacks.traceCmpLong((long) arguments[0], (long) arguments[1], hookId); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals") + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", + targetMethod = "equalsIgnoreCase") + public static void + equals(MethodHandle method, String thisObject, Object[] arguments, int hookId, Boolean areEqual) { + if (!areEqual && arguments.length == 1 && arguments[0] instanceof String) { + // The precise value of the result of the comparison is not used by libFuzzer as long as it is + // non-zero. + TraceDataFlowNativeCallbacks.traceStrcmp(thisObject, (String) arguments[0], 1, hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.Object", targetMethod = "equals") + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.CharSequence", targetMethod = "equals") + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.Number", targetMethod = "equals") + public static void + genericEquals( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean areEqual) { + if (!areEqual && arguments.length == 1 && arguments[0] != null + && thisObject.getClass() == arguments[0].getClass()) { + TraceDataFlowNativeCallbacks.traceGenericCmp(thisObject, arguments[0], hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "clojure.lang.Util", targetMethod = "equiv") + public static void genericStaticEquals( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean areEqual) { + if (!areEqual && arguments.length == 2 && arguments[0] != null && arguments[1] != null + && arguments[1].getClass() == arguments[0].getClass()) { + TraceDataFlowNativeCallbacks.traceGenericCmp(arguments[0], arguments[1], hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "compareTo") + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", + targetMethod = "compareToIgnoreCase") + public static void + compareTo( + MethodHandle method, String thisObject, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue != 0 && arguments.length == 1 && arguments[0] instanceof String) { + TraceDataFlowNativeCallbacks.traceStrcmp( + thisObject, (String) arguments[0], returnValue, hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "contentEquals") + public static void + contentEquals(MethodHandle method, String thisObject, Object[] arguments, int hookId, + Boolean areEqualContents) { + if (!areEqualContents && arguments.length == 1 && arguments[0] instanceof CharSequence) { + TraceDataFlowNativeCallbacks.traceStrcmp( + thisObject, ((CharSequence) arguments[0]).toString(), 1, hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", + targetMethod = "regionMatches", targetMethodDescriptor = "(ZILjava/lang/String;II)Z") + public static void + regionsMatches5( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) { + if (!returnValue) { + int toffset = (int) arguments[1]; + String other = (String) arguments[2]; + int ooffset = (int) arguments[3]; + int len = (int) arguments[4]; + regionMatchesInternal((String) thisObject, toffset, other, ooffset, len, hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", + targetMethod = "regionMatches", targetMethodDescriptor = "(ILjava/lang/String;II)Z") + public static void + regionMatches4( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) { + if (!returnValue) { + int toffset = (int) arguments[0]; + String other = (String) arguments[1]; + int ooffset = (int) arguments[2]; + int len = (int) arguments[3]; + regionMatchesInternal((String) thisObject, toffset, other, ooffset, len, hookId); + } + } + + private static void regionMatchesInternal( + String thisString, int toffset, String other, int ooffset, int len, int hookId) { + if (toffset < 0 || ooffset < 0) + return; + int cappedThisStringEnd = Math.min(toffset + len, thisString.length()); + int cappedOtherStringEnd = Math.min(ooffset + len, other.length()); + String thisPart = thisString.substring(toffset, cappedThisStringEnd); + String otherPart = other.substring(ooffset, cappedOtherStringEnd); + TraceDataFlowNativeCallbacks.traceStrcmp(thisPart, otherPart, 1, hookId); + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "contains") + public static void + contains( + MethodHandle method, String thisObject, Object[] arguments, int hookId, Boolean doesContain) { + if (!doesContain && arguments.length == 1 && arguments[0] instanceof CharSequence) { + TraceDataFlowNativeCallbacks.traceStrstr( + thisObject, ((CharSequence) arguments[0]).toString(), hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "indexOf") + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "lastIndexOf") + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.StringBuffer", targetMethod = "indexOf") + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.StringBuffer", + targetMethod = "lastIndexOf") + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.StringBuilder", targetMethod = "indexOf") + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.StringBuilder", + targetMethod = "lastIndexOf") + public static void + indexOf( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue == -1 && arguments.length >= 1 && arguments[0] instanceof String) { + TraceDataFlowNativeCallbacks.traceStrstr( + thisObject.toString(), (String) arguments[0], hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "startsWith") + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "endsWith") + public static void + startsWith(MethodHandle method, String thisObject, Object[] arguments, int hookId, + Boolean doesStartOrEndsWith) { + if (!doesStartOrEndsWith && arguments.length >= 1 && arguments[0] instanceof String) { + TraceDataFlowNativeCallbacks.traceStrstr(thisObject, (String) arguments[0], hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "replace", + targetMethodDescriptor = + "(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;") + public static void + replace( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, String returnValue) { + String original = (String) thisObject; + // Report only if the replacement was not successful. + if (original.equals(returnValue)) { + String target = arguments[0].toString(); + TraceDataFlowNativeCallbacks.traceStrstr(original, target, hookId); + } + } + + // For standard Kotlin packages, which are named according to the pattern kotlin.*, we append a + // whitespace to the package name of the target class so that they are not mangled due to shading. + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.jvm.internal.Intrinsics ", + targetMethod = "areEqual") + @MethodHook( + type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "equals") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "equals$default") + public static void + equalsKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, + Boolean equalStrings) { + if (!equalStrings && arguments.length >= 2 && arguments[0] instanceof String + && arguments[1] instanceof String) { + TraceDataFlowNativeCallbacks.traceStrcmp( + (String) arguments[0], (String) arguments[1], 1, hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "contentEquals") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "contentEquals$default") + public static void + contentEqualKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, + Boolean equalStrings) { + if (!equalStrings && arguments.length >= 2 && arguments[0] instanceof CharSequence + && arguments[1] instanceof CharSequence) { + TraceDataFlowNativeCallbacks.traceStrcmp( + arguments[0].toString(), arguments[1].toString(), 1, hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "compareTo") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "compareTo$default") + public static void + compareToKt( + MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue != 0 && arguments.length >= 2 && arguments[0] instanceof String + && arguments[1] instanceof String) { + TraceDataFlowNativeCallbacks.traceStrcmp( + (String) arguments[0], (String) arguments[1], 1, hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "endsWith") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "endsWith$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "startsWith") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "startsWith$default") + public static void + startsWithKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, + Boolean doesStartOrEndsWith) { + if (!doesStartOrEndsWith && arguments.length >= 2 && arguments[0] instanceof CharSequence + && arguments[1] instanceof CharSequence) { + TraceDataFlowNativeCallbacks.traceStrstr( + arguments[0].toString(), arguments[1].toString(), hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "contains") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "contains$default") + public static void + containsKt( + MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Boolean doesContain) { + if (!doesContain && arguments.length >= 2 && arguments[0] instanceof CharSequence + && arguments[1] instanceof CharSequence) { + TraceDataFlowNativeCallbacks.traceStrstr( + arguments[0].toString(), arguments[1].toString(), hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "indexOf") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "indexOf$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "lastIndexOf") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "lastIndexOf$default") + public static void + indexOfKt( + MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue != -1 || arguments.length < 2 || !(arguments[0] instanceof CharSequence)) { + return; + } + if (arguments[1] instanceof String) { + TraceDataFlowNativeCallbacks.traceStrstr( + arguments[0].toString(), (String) arguments[1], hookId); + } else if (arguments[1] instanceof Character) { + TraceDataFlowNativeCallbacks.traceStrstr( + arguments[0].toString(), ((Character) arguments[1]).toString(), hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "replace") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replace$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceAfter") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceAfter$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceAfterLast") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceAfterLast$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceBefore") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceBefore$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceBeforeLast") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceBeforeLast$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceFirst") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceFirst$default") + public static void + replaceKt( + MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, String returnValue) { + if (arguments.length < 2 || !(arguments[0] instanceof String)) { + return; + } + String original = (String) arguments[0]; + if (!original.equals(returnValue)) { + return; + } + + // We currently don't handle the overloads that take a regex as a second argument. + if (arguments[1] instanceof String || arguments[1] instanceof Character) { + TraceDataFlowNativeCallbacks.traceStrstr(original, arguments[1].toString(), hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "regionMatches", + targetMethodDescriptor = "(Ljava/lang/String;ILjava/lang/String;IIZ)Z") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "regionMatches$default", + targetMethodDescriptor = "(Ljava/lang/String;ILjava/lang/String;IIZILjava/lang/Object;)Z") + public static void + regionMatchesKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, + Boolean doesRegionMatch) { + if (!doesRegionMatch) { + String thisString = arguments[0].toString(); + int thisOffset = (int) arguments[1]; + String other = arguments[2].toString(); + int otherOffset = (int) arguments[3]; + int length = (int) arguments[4]; + regionMatchesInternal(thisString, thisOffset, other, otherOffset, length, hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "indexOfAny") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "indexOfAny$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "lastIndexOfAny") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "lastIndexOfAny$default") + public static void + indexOfAnyKt( + MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue == -1 && arguments.length >= 2 && arguments[0] instanceof CharSequence) { + guideTowardContainmentOfFirstElement(arguments[0].toString(), arguments[1], hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "findAnyOf") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "findAnyOf$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "findLastAnyOf") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "findLastAnyOf$default") + public static void + findAnyKt( + MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Object returnValue) { + if (returnValue == null && arguments.length >= 2 && arguments[0] instanceof CharSequence) { + guideTowardContainmentOfFirstElement(arguments[0].toString(), arguments[1], hookId); + } + } + + private static void guideTowardContainmentOfFirstElement( + String containingString, Object candidateCollectionObj, int hookId) { + if (candidateCollectionObj instanceof Collection<?>) { + Collection<?> strings = (Collection<?>) candidateCollectionObj; + if (strings.isEmpty()) { + return; + } + Object firstElementObj = strings.iterator().next(); + if (firstElementObj instanceof CharSequence) { + TraceDataFlowNativeCallbacks.traceStrstr( + containingString, firstElementObj.toString(), hookId); + } + } else if (candidateCollectionObj.getClass().isArray()) { + if (candidateCollectionObj.getClass().getComponentType() == char.class) { + char[] chars = (char[]) candidateCollectionObj; + if (chars.length > 0) { + TraceDataFlowNativeCallbacks.traceStrstr( + containingString, Character.toString(chars[0]), hookId); + } + } + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "equals", + targetMethodDescriptor = "([B[B)Z") + public static void + arraysEquals( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) { + if (returnValue) + return; + byte[] first = (byte[]) arguments[0]; + byte[] second = (byte[]) arguments[1]; + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "equals", + targetMethodDescriptor = "([BII[BII)Z") + public static void + arraysEqualsRange( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) { + if (returnValue) + return; + byte[] first = + Arrays.copyOfRange((byte[]) arguments[0], (int) arguments[1], (int) arguments[2]); + byte[] second = + Arrays.copyOfRange((byte[]) arguments[3], (int) arguments[4], (int) arguments[5]); + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "compare", + targetMethodDescriptor = "([B[B)I") + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", + targetMethod = "compareUnsigned", targetMethodDescriptor = "([B[B)I") + public static void + arraysCompare( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue == 0) + return; + byte[] first = (byte[]) arguments[0]; + byte[] second = (byte[]) arguments[1]; + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "compare", + targetMethodDescriptor = "([BII[BII)I") + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", + targetMethod = "compareUnsigned", targetMethodDescriptor = "([BII[BII)I") + public static void + arraysCompareRange( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue == 0) + return; + byte[] first = + Arrays.copyOfRange((byte[]) arguments[0], (int) arguments[1], (int) arguments[2]); + byte[] second = + Arrays.copyOfRange((byte[]) arguments[3], (int) arguments[4], (int) arguments[5]); + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId); + } + + // The maximal number of elements of a non-TreeMap Map that will be sorted and searched for the + // key closest to the current lookup key in the mapGet hook. + private static final int MAX_NUM_KEYS_TO_ENUMERATE = 100; + + @SuppressWarnings({"rawtypes", "unchecked"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Map", targetMethod = "get") + public static void mapGet( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) { + if (returnValue != null) + return; + if (arguments.length != 1) { + return; + } + if (thisObject == null) + return; + final Map map = (Map) thisObject; + if (map.size() == 0) + return; + final Object currentKey = arguments[0]; + if (currentKey == null) + return; + // Find two valid map keys that bracket currentKey. + // This is a generalization of libFuzzer's __sanitizer_cov_trace_switch: + // https://github.com/llvm/llvm-project/blob/318942de229beb3b2587df09e776a50327b5cef0/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp#L564 + Object lowerBoundKey = null; + Object upperBoundKey = null; + try { + if (map instanceof TreeMap) { + final TreeMap treeMap = (TreeMap) map; + try { + lowerBoundKey = treeMap.floorKey(currentKey); + upperBoundKey = treeMap.ceilingKey(currentKey); + } catch (ClassCastException ignored) { + // Can be thrown by floorKey and ceilingKey if currentKey is of a type that can't be + // compared to the maps keys. + } + } else if (currentKey instanceof Comparable) { + final Comparable comparableCurrentKey = (Comparable) currentKey; + // Find two keys that bracket currentKey. + // Note: This is not deterministic if map.size() > MAX_NUM_KEYS_TO_ENUMERATE. + int enumeratedKeys = 0; + for (Object validKey : map.keySet()) { + if (!(validKey instanceof Comparable)) + continue; + final Comparable comparableValidKey = (Comparable) validKey; + // If the key sorts lower than the non-existing key, but higher than the current lower + // bound, update the lower bound and vice versa for the upper bound. + try { + if (comparableValidKey.compareTo(comparableCurrentKey) < 0 + && (lowerBoundKey == null || comparableValidKey.compareTo(lowerBoundKey) > 0)) { + lowerBoundKey = validKey; + } + if (comparableValidKey.compareTo(comparableCurrentKey) > 0 + && (upperBoundKey == null || comparableValidKey.compareTo(upperBoundKey) < 0)) { + upperBoundKey = validKey; + } + } catch (ClassCastException ignored) { + // Can be thrown by floorKey and ceilingKey if currentKey is of a type that can't be + // compared to the maps keys. + } + if (enumeratedKeys++ > MAX_NUM_KEYS_TO_ENUMERATE) + break; + } + } + } catch (ConcurrentModificationException ignored) { + // map was modified by another thread, skip this invocation + return; + } + // Modify the hook ID so that compares against distinct valid keys are traced separately. + if (lowerBoundKey != null) { + TraceDataFlowNativeCallbacks.traceGenericCmp( + currentKey, lowerBoundKey, hookId + lowerBoundKey.hashCode()); + } + if (upperBoundKey != null) { + TraceDataFlowNativeCallbacks.traceGenericCmp( + currentKey, upperBoundKey, hookId + upperBoundKey.hashCode()); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "org.junit.jupiter.api.Assertions", + targetMethod = "assertNotEquals", + targetMethodDescriptor = "(Ljava/lang/Object;Ljava/lang/Object;)V") + @MethodHook(type = HookType.AFTER, targetClassName = "org.junit.jupiter.api.Assertions", + targetMethod = "assertNotEquals", + targetMethodDescriptor = "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/String;)V") + @MethodHook(type = HookType.AFTER, targetClassName = "org.junit.jupiter.api.Assertions", + targetMethod = "assertNotEquals", + targetMethodDescriptor = + "(Ljava/lang/Object;Ljava/lang/Object;Ljava/util/function/Supplier;)V") + public static void + assertEquals(MethodHandle method, Object node, Object[] args, int hookId, Object alwaysNull) { + if (args[0] != null && args[1] != null && args[0].getClass() == args[1].getClass()) { + TraceDataFlowNativeCallbacks.traceGenericCmp(args[0], args[1], hookId); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java b/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java new file mode 100644 index 00000000..777eb0a2 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java @@ -0,0 +1,125 @@ +// Copyright 2021 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.runtime; + +import com.github.fmeum.rules_jni.RulesJni; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.util.Arrays; +import org.objectweb.asm.Type; + +@SuppressWarnings("unused") +final public class TraceDataFlowNativeCallbacks { + // Note that we are not encoding as modified UTF-8 here: The FuzzedDataProvider transparently + // converts CESU8 into modified UTF-8 by coding null bytes on two bytes. Since the fuzzer is more + // likely to insert literal null bytes, having both the fuzzer input and the reported string + // comparisons be CESU8 should perform even better than the current implementation using modified + // UTF-8. + private static final Charset FUZZED_DATA_CHARSET = Charset.forName("CESU8"); + + static { + RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver"); + } + + // It is possible for RulesJni#loadLibrary to trigger a hook even though it isn't instrumented if + // it uses regexes, which it does with at least some JDKs due to its use of String#format. This + // led to exceptions in the past when the hook ended up calling traceStrcmp or traceStrstr before + // the static initializer was run: FUZZED_DATA_CHARSET used to be initialized after the call and + // thus still had the value null when encodeForLibFuzzer was called, resulting in an NPE in + // String#getBytes(Charset). Just switching the order may actually make this bug worse: It could + // now lead to traceMemcmp being called before the native library has been loaded. We guard + // against this by making the hooks noops when static initialization of this class hasn't + // completed yet. + private static final boolean NATIVE_INITIALIZED = true; + + public static native void traceMemcmp(byte[] b1, byte[] b2, int result, int pc); + + public static void traceStrcmp(String s1, String s2, int result, int pc) { + if (NATIVE_INITIALIZED) { + traceMemcmp(encodeForLibFuzzer(s1), encodeForLibFuzzer(s2), result, pc); + } + } + + public static void traceStrstr(String s1, String s2, int pc) { + if (NATIVE_INITIALIZED) { + traceStrstr0(encodeForLibFuzzer(s2), pc); + } + } + + public static void traceReflectiveCall(Executable callee, int pc) { + String className = callee.getDeclaringClass().getCanonicalName(); + String executableName = callee.getName(); + String descriptor; + if (callee instanceof Method) { + descriptor = Type.getMethodDescriptor((Method) callee); + } else { + descriptor = Type.getConstructorDescriptor((Constructor<?>) callee); + } + tracePcIndir(Arrays.hashCode(new String[] {className, executableName, descriptor}), pc); + } + + public static int traceCmpLongWrapper(long arg1, long arg2, int pc) { + traceCmpLong(arg1, arg2, pc); + // Long.compare serves as a substitute for the lcmp opcode, which can't be used directly + // as the stack layout required for the call can't be achieved without local variables. + return Long.compare(arg1, arg2); + } + + // The caller has to ensure that arg1 and arg2 have the same class. + public static void traceGenericCmp(Object arg1, Object arg2, int pc) { + if (arg1 instanceof CharSequence) { + traceStrcmp(arg1.toString(), arg2.toString(), 1, pc); + } else if (arg1 instanceof Integer) { + traceCmpInt((int) arg1, (int) arg2, pc); + } else if (arg1 instanceof Long) { + traceCmpLong((long) arg1, (long) arg2, pc); + } else if (arg1 instanceof Short) { + traceCmpInt((short) arg1, (short) arg2, pc); + } else if (arg1 instanceof Byte) { + traceCmpInt((byte) arg1, (byte) arg2, pc); + } else if (arg1 instanceof Character) { + traceCmpInt((char) arg1, (char) arg2, pc); + } else if (arg1 instanceof Number) { + traceCmpLong(((Number) arg1).longValue(), ((Number) arg2).longValue(), pc); + } else if (arg1 instanceof byte[]) { + traceMemcmp((byte[]) arg1, (byte[]) arg2, 1, pc); + } + } + + /* trace-cmp */ + public static native void traceCmpInt(int arg1, int arg2, int pc); + public static native void traceConstCmpInt(int arg1, int arg2, int pc); + public static native void traceCmpLong(long arg1, long arg2, int pc); + public static native void traceSwitch(long val, long[] cases, int pc); + /* trace-div */ + public static native void traceDivInt(int val, int pc); + public static native void traceDivLong(long val, int pc); + /* trace-gep */ + public static native void traceGep(long val, int pc); + /* indirect-calls */ + public static native void tracePcIndir(int callee, int caller); + + public static native void handleLibraryLoad(); + + private static byte[] encodeForLibFuzzer(String str) { + // libFuzzer string hooks only ever consume the first 64 bytes, so we can definitely cut the + // string off after 64 characters. + return str.substring(0, Math.min(str.length(), 64)).getBytes(FUZZED_DATA_CHARSET); + } + + private static native void traceStrstr0(byte[] needle, int pc); +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/TraceDivHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/TraceDivHooks.java new file mode 100644 index 00000000..c4991eb5 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/TraceDivHooks.java @@ -0,0 +1,47 @@ +// Copyright 2021 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.runtime; + +import com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; + +@SuppressWarnings("unused") +final public class TraceDivHooks { + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer", + targetMethod = "divideUnsigned", targetMethodDescriptor = "(II)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer", + targetMethod = "remainderUnsigned", targetMethodDescriptor = "(II)I") + public static void + intUnsignedDivide(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + // Since the arguments are to be treated as unsigned integers we need a long to fit the + // divisor. + TraceDataFlowNativeCallbacks.traceDivLong(Integer.toUnsignedLong((int) arguments[1]), hookId); + } + + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long", + targetMethod = "divideUnsigned", targetMethodDescriptor = "(JJ)J") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long", + targetMethod = "remainderUnsigned", targetMethodDescriptor = "(JJ)J") + public static void + longUnsignedDivide(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + long divisor = (long) arguments[1]; + // Run the callback only if the divisor, which is regarded as an unsigned long, fits in a + // signed long, i.e., does not have the sign bit set. + if (divisor > 0) { + TraceDataFlowNativeCallbacks.traceDivLong(divisor, hookId); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/TraceIndirHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/TraceIndirHooks.java new file mode 100644 index 00000000..897ede6c --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/TraceIndirHooks.java @@ -0,0 +1,35 @@ +// Copyright 2021 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.runtime; + +import com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Executable; + +@SuppressWarnings("unused") +final public class TraceIndirHooks { + // The reflection hook is of type AFTER as it should only report calls that did not fail because + // of incorrect arguments passed. + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.reflect.Method", targetMethod = "invoke") + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.reflect.Constructor", + targetMethod = "newInstance") + public static void + methodInvoke( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) { + TraceDataFlowNativeCallbacks.traceReflectiveCall((Executable) thisObject, hookId); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/bootstrap_shade_rules b/src/main/java/com/code_intelligence/jazzer/runtime/bootstrap_shade_rules new file mode 100644 index 00000000..0cafcf0a --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/bootstrap_shade_rules @@ -0,0 +1,4 @@ +rule com.github.fmeum.rules_jni.** com.code_intelligence.jazzer.bootstrap.@0 +rule kotlin.** com.code_intelligence.jazzer.bootstrap.@0 +rule net.sf.jsqlparser.** com.code_intelligence.jazzer.bootstrap.@0 +rule org.objectweb.asm.** com.code_intelligence.jazzer.bootstrap.@0 diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/verify_shading.sh b/src/main/java/com/code_intelligence/jazzer/runtime/verify_shading.sh new file mode 100755 index 00000000..b3a74ea9 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/verify_shading.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env sh +# Copyright 2022 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. + +[ -f "$1" ] || exit 1 +# List all files in the jar and exclude an allowed list of files. +# Since grep fails if there is no match, ! ... | grep ... fails if there is a +# match. +! external/local_jdk/bin/jar tf "$1" | \ + grep -v \ + -e '^com/$' \ + -e '^com/code_intelligence/$' \ + -e '^com/code_intelligence/jazzer/' \ + -e '^jaz/' \ + -e '^META-INF/$' \ + -e '^META-INF/MANIFEST.MF$' diff --git a/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel new file mode 100644 index 00000000..ff9e0d3c --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel @@ -0,0 +1,70 @@ +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") +load("//bazel:kotlin.bzl", "ktlint") + +kt_jvm_library( + name = "utils", + srcs = ["Utils.kt"], + visibility = ["//visibility:public"], +) + +kt_jvm_library( + name = "class_name_globber", + srcs = ["ClassNameGlobber.kt"], + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/instrumentor:__pkg__", + ], + deps = [":simple_glob_matcher"], +) + +java_library( + name = "log", + srcs = ["Log.java"], + visibility = ["//visibility:public"], +) + +kt_jvm_library( + name = "manifest_utils", + srcs = ["ManifestUtils.kt"], + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/driver:__pkg__", + ], + deps = [":log"], +) + +kt_jvm_library( + name = "simple_glob_matcher", + srcs = ["SimpleGlobMatcher.kt"], + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/autofuzz:__pkg__", + ], +) + +java_library( + name = "unsafe_provider", + srcs = ["UnsafeProvider.java"], + visibility = [ + "//:__subpackages__", + ], +) + +java_library( + name = "unsafe_utils", + srcs = ["UnsafeUtils.java"], + visibility = [ + "//:__subpackages__", + ], + deps = [ + ":unsafe_provider", + "@org_ow2_asm_asm//jar", + ], +) + +java_library( + name = "zip_utils", + srcs = ["ZipUtils.java"], + visibility = ["//visibility:public"], +) + +ktlint() diff --git a/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt b/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt new file mode 100644 index 00000000..c6fa20a7 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt @@ -0,0 +1,66 @@ +// Copyright 2021 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.utils + +private val BASE_INCLUDED_CLASS_NAME_GLOBS = listOf( + "**", // everything +) + +// We use both a strong indicator for running as a Bazel test together with an indicator for a +// Bazel coverage run to rule out false positives. +private val IS_BAZEL_COVERAGE_RUN = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") != null && + System.getenv("COVERAGE_DIR") != null + +private val ADDITIONAL_EXCLUDED_NAME_GLOBS_FOR_BAZEL_COVERAGE = listOf( + "com.google.testing.coverage.**", + "org.jacoco.**", +) + +private val BASE_EXCLUDED_CLASS_NAME_GLOBS = listOf( + // JDK internals + "\\[**", // array types + "java.**", + "javax.**", + "jdk.**", + "sun.**", + "com.sun.**", // package for Proxy objects + // Azul JDK internals + "com.azul.tooling.**", + // Kotlin internals + "kotlin.**", + // Jazzer internals + "com.code_intelligence.jazzer.**", + "jaz.Ter", // safe companion of the honeypot class used by sanitizers + "jaz.Zer", // honeypot class used by sanitizers + // Test and instrumentation tools + "org.junit.**", // dependency of @FuzzTest + "org.mockito.**", // can cause instrumentation cycles + "net.bytebuddy.**", // ignore Byte Buddy, though it's probably shaded + "org.jetbrains.**", // ignore JetBrains products (coverage agent) +) + if (IS_BAZEL_COVERAGE_RUN) ADDITIONAL_EXCLUDED_NAME_GLOBS_FOR_BAZEL_COVERAGE else listOf() + +class ClassNameGlobber(includes: List<String>, excludes: List<String>) { + // If no include globs are provided, start with all classes. + private val includeMatchers = includes.ifEmpty { BASE_INCLUDED_CLASS_NAME_GLOBS } + .map(::SimpleGlobMatcher) + + // If no include globs are provided, additionally exclude stdlib classes as well as our own classes. + private val excludeMatchers = (if (includes.isEmpty()) BASE_EXCLUDED_CLASS_NAME_GLOBS + excludes else excludes) + .map(::SimpleGlobMatcher) + + fun includes(className: String): Boolean { + return includeMatchers.any { it.matches(className) } && excludeMatchers.none { it.matches(className) } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/utils/Log.java b/src/main/java/com/code_intelligence/jazzer/utils/Log.java new file mode 100644 index 00000000..bccd3a32 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/utils/Log.java @@ -0,0 +1,104 @@ +/* + * 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.utils; + +import java.io.PrintStream; + +/** + * Provides static functions that should be used for any kind of output (structured or unstructured) + * emitted by the fuzzer. + * + * <p>Output is printed to {@link System#err} and {@link System#out} until {@link + * Log#fixOutErr(PrintStream, PrintStream)} is called, which locks in the {@link PrintStream}s to be + * used from there on. + */ +public class Log { + // Don't use directly, always use getOut() and getErr() instead - when these fields haven't been + // set yet, we want to resolve them dynamically as System.out and System.err, which may change + // over the course of the fuzzer's lifetime. + private static PrintStream fixedOut; + private static PrintStream fixedErr; + + /** + * The {@link PrintStream}s to use for all output from this call on. + */ + public static void fixOutErr(PrintStream out, PrintStream err) { + if (out == null) { + throw new IllegalArgumentException("out must not be null"); + } + if (err == null) { + throw new IllegalArgumentException("err must not be null"); + } + Log.fixedOut = out; + Log.fixedErr = err; + } + + public static void println(String message) { + getErr().println(message); + } + + public static void structuredOutput(String output) { + getOut().println(output); + } + + public static void info(String message) { + println("INFO: ", message, null); + } + + public static void warn(String message) { + warn(message, null); + } + + public static void warn(String message, Throwable t) { + println("WARN: ", message, t); + } + + public static void error(String message) { + error(message, null); + } + + public static void error(Throwable t) { + error(null, t); + } + + public static void error(String message, Throwable t) { + println("ERROR: ", message, t); + } + + public static void finding(Throwable t) { + println("\n== Java Exception: ", null, t); + } + + private static void println(String prefix, String message, Throwable t) { + PrintStream err = getErr(); + err.print(prefix); + if (message != null) { + err.println(message + (t != null ? ":" : "")); + } + if (t != null) { + t.printStackTrace(err); + } + } + + private static PrintStream getOut() { + return fixedOut != null ? fixedOut : System.out; + } + + private static PrintStream getErr() { + return fixedErr != null ? fixedErr : System.err; + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt b/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt new file mode 100644 index 00000000..9d413a0e --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt @@ -0,0 +1,50 @@ +// Copyright 2021 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.utils + +import java.util.jar.Manifest + +object ManifestUtils { + + private const val FUZZ_TARGET_CLASS = "Jazzer-Fuzz-Target-Class" + const val HOOK_CLASSES = "Jazzer-Hook-Classes" + + fun combineManifestValues(attribute: String): List<String> { + val manifests = ManifestUtils::class.java.classLoader.getResources("META-INF/MANIFEST.MF") + return manifests.asSequence().mapNotNull { url -> + url.openStream().use { inputStream -> + val manifest = Manifest(inputStream) + manifest.mainAttributes.getValue(attribute) + } + }.toList() + } + + /** + * Returns the value of the `Fuzz-Target-Class` manifest attribute if there is a unique one among all manifest + * files in the classpath. + */ + @JvmStatic + fun detectFuzzTargetClass(): String? { + val fuzzTargets = combineManifestValues(FUZZ_TARGET_CLASS) + return when (fuzzTargets.size) { + 0 -> null + 1 -> fuzzTargets.first() + else -> { + Log.warn("More than one Jazzer-Fuzz-Target-Class manifest entry detected on the classpath.") + null + } + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/utils/SimpleGlobMatcher.kt b/src/main/java/com/code_intelligence/jazzer/utils/SimpleGlobMatcher.kt new file mode 100644 index 00000000..fb497fda --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/utils/SimpleGlobMatcher.kt @@ -0,0 +1,71 @@ +// Copyright 2022 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.utils + +class SimpleGlobMatcher(val glob: String) { + private enum class Type { + // foo.bar (matches foo.bar only) + FULL_MATCH, + + // foo.** (matches foo.bar and foo.bar.baz) + PATH_WILDCARD_SUFFIX, + + // foo.* (matches foo.bar, but not foo.bar.baz) + SEGMENT_WILDCARD_SUFFIX, + } + + private val type: Type + private val prefix: String + + init { + // Remain compatible with globs such as "\\[" that use escaping. + val pattern = glob.replace("\\", "") + when { + !pattern.contains('*') -> { + type = Type.FULL_MATCH + prefix = pattern + } + // Ends with "**" and contains no other '*'. + pattern.endsWith("**") && pattern.indexOf('*') == pattern.length - 2 -> { + type = Type.PATH_WILDCARD_SUFFIX + prefix = pattern.removeSuffix("**") + } + // Ends with "*" and contains no other '*'. + pattern.endsWith('*') && pattern.indexOf('*') == pattern.length - 1 -> { + type = Type.SEGMENT_WILDCARD_SUFFIX + prefix = pattern.removeSuffix("*") + } + else -> throw IllegalArgumentException( + "Unsupported glob pattern (only foo.bar, foo.* and foo.** are supported): $pattern", + ) + } + } + + /** + * Checks whether [maybeInternalClassName], which may be internal (foo/bar) or not (foo.bar), matches [glob]. + */ + fun matches(maybeInternalClassName: String): Boolean { + val className = maybeInternalClassName.replace('/', '.') + return when (type) { + Type.FULL_MATCH -> className == prefix + Type.PATH_WILDCARD_SUFFIX -> className.startsWith(prefix) + Type.SEGMENT_WILDCARD_SUFFIX -> { + // className starts with prefix and contains no further '.'. + className.startsWith(prefix) && + className.indexOf('.', startIndex = prefix.length) == -1 + } + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/utils/UnsafeProvider.java b/src/main/java/com/code_intelligence/jazzer/utils/UnsafeProvider.java new file mode 100644 index 00000000..e36e64c4 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/utils/UnsafeProvider.java @@ -0,0 +1,56 @@ +// Copyright 2022 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.utils; + +import java.lang.reflect.Field; +import java.util.Arrays; +import sun.misc.Unsafe; + +public final class UnsafeProvider { + private static final Unsafe UNSAFE = getUnsafeInternal(); + + public static Unsafe getUnsafe() { + return UNSAFE; + } + + private static Unsafe getUnsafeInternal() { + try { + // The Jazzer runtime is loaded by the bootstrap class loader and should thus pass the + // security checks in getUnsafe, so try that first. + return Unsafe.getUnsafe(); + } catch (Throwable unused) { + // If not running as an agent, use the classical reflection trick to get an Unsafe instance, + // taking into account that the private field may have a name other than "theUnsafe": + // https://android.googlesource.com/platform/libcore/+/gingerbread/luni/src/main/java/sun/misc/Unsafe.java#32 + for (Field f : Unsafe.class.getDeclaredFields()) { + if (f.getType() == Unsafe.class) { + f.setAccessible(true); + try { + return (Unsafe) f.get(null); + } catch (IllegalAccessException e) { + throw new IllegalStateException( + "Please file a bug at https://github.com/CodeIntelligenceTesting/jazzer/issues/new " + + "with this information: Failed to access Unsafe member on Unsafe class", + e); + } + } + } + throw new IllegalStateException(String.format( + "Please file a bug at https://github.com/CodeIntelligenceTesting/jazzer/issues/new with " + + "this information: Failed to find Unsafe member on Unsafe class, have: " + + Arrays.deepToString(Unsafe.class.getDeclaredFields()))); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/utils/UnsafeUtils.java b/src/main/java/com/code_intelligence/jazzer/utils/UnsafeUtils.java new file mode 100644 index 00000000..30c88dc9 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/utils/UnsafeUtils.java @@ -0,0 +1,78 @@ +/* + * 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.utils; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Optional; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; + +public final class UnsafeUtils { + /** + * Dynamically creates a concrete class implementing the given abstract class. + * + * <p>The returned class will not be functional and should only be used to construct instances + * via {@link sun.misc.Unsafe#allocateInstance(Class)}. + */ + public static <T> Class<? extends T> defineAnonymousConcreteSubclass(Class<T> abstractClass) { + if (!Modifier.isAbstract(abstractClass.getModifiers())) { + throw new IllegalArgumentException(abstractClass + " is not abstract"); + } + + ClassWriter cw = new ClassWriter(0); + String superClassName = abstractClass.getName().replace('.', '/'); + // Only the package of the class name matters, the actual name is generated. defineHiddenClass + // requires the package of the new class to match the one of the lookup. + String className = UnsafeUtils.class.getPackage().getName().replace('.', '/') + "/Anonymous"; + cw.visit(Opcodes.V1_8, 0, className, null, superClassName, null); + cw.visitEnd(); + + try { + Optional<Method> defineHiddenClass = + Arrays.stream(Lookup.class.getMethods()) + .filter(method -> method.getName().equals("defineHiddenClass")) + .findFirst(); + Optional<Class<?>> classOption = + Arrays.stream(Lookup.class.getClasses()) + .filter(clazz -> clazz.getSimpleName().equals("ClassOption")) + .findFirst(); + // MethodHandles.Lookup#defineHiddenClass is available as of Java 15. + // Unsafe#defineAnonymousClass has been removed in Java 17. + if (defineHiddenClass.isPresent() && classOption.isPresent()) { + return ((MethodHandles.Lookup) defineHiddenClass.get().invoke(MethodHandles.lookup(), + cw.toByteArray(), true, Array.newInstance(classOption.get(), 0))) + .lookupClass() + .asSubclass(abstractClass); + } else { + return (Class<? extends T>) UnsafeProvider.getUnsafe() + .getClass() + .getMethod("defineAnonymousClass", Class.class, byte[].class, Object[].class) + .invoke(UnsafeProvider.getUnsafe(), UnsafeUtils.class, cw.toByteArray(), null); + } + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IllegalStateException(e); + } + } + + private UnsafeUtils() {} +} diff --git a/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt b/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt new file mode 100644 index 00000000..2de47820 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt @@ -0,0 +1,45 @@ +// Copyright 2021 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. +@file:JvmName("Utils") + +package com.code_intelligence.jazzer.utils + +import java.lang.reflect.Executable + +val Class<*>.readableDescriptor: String + get() = when { + isPrimitive -> { + when (this) { + Boolean::class.javaPrimitiveType -> "boolean" + Byte::class.javaPrimitiveType -> "byte" + Char::class.javaPrimitiveType -> "char" + Short::class.javaPrimitiveType -> "short" + Int::class.javaPrimitiveType -> "int" + Long::class.javaPrimitiveType -> "long" + Float::class.javaPrimitiveType -> "float" + Double::class.javaPrimitiveType -> "double" + java.lang.Void::class.javaPrimitiveType -> "void" + else -> throw IllegalStateException("Unknown primitive type: $name") + } + } + isArray -> "${componentType.readableDescriptor}[]" + java.lang.Object::class.java.isAssignableFrom(this) -> name + else -> throw IllegalArgumentException("Unknown class type: $name") + } + +// This does not include the return type as the parameter descriptors already uniquely identify the executable. +val Executable.readableDescriptor: String + get() = parameterTypes.joinToString(separator = ",", prefix = "(", postfix = ")") { parameterType -> + parameterType.readableDescriptor + } diff --git a/src/main/java/com/code_intelligence/jazzer/utils/ZipUtils.java b/src/main/java/com/code_intelligence/jazzer/utils/ZipUtils.java new file mode 100644 index 00000000..4da35c3f --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/utils/ZipUtils.java @@ -0,0 +1,126 @@ +/* + * 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.utils; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.IllegalArgumentException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +public final class ZipUtils { + private ZipUtils() {} + + public static Set<String> mergeZipToZip(String src, ZipOutputStream zos, Set<String> skipFiles) + throws IOException { + HashSet<String> filesAdded = new HashSet<>(); + try (JarFile jarFile = new JarFile(src)) { + // Copy entries from src to dst (jarFile to ZipOutputStream) + Enumeration<JarEntry> allEntries = jarFile.entries(); + while (allEntries.hasMoreElements()) { + JarEntry entry = allEntries.nextElement(); + if (skipFiles != null && skipFiles.contains(entry.getName())) { + continue; + } + + zos.putNextEntry(new ZipEntry(entry.getName())); + try (InputStream is = jarFile.getInputStream(entry)) { + byte[] buf = new byte[1024]; + int i = 0; + while ((i = is.read(buf)) != -1) { + zos.write(buf, 0, i); + } + + zos.closeEntry(); + filesAdded.add(entry.getName()); + } + } + } + + return filesAdded; + } + + public static Set<String> mergeDirectoryToZip(String src, ZipOutputStream zos, + Set<String> skipFiles) throws IllegalArgumentException, IOException { + HashSet<String> filesAdded = new HashSet<>(); + File sourceDir = new File(src); + if (!sourceDir.isDirectory()) { + throw new IllegalArgumentException("Argument src must be a directory."); + } + + Files.walkFileTree(sourceDir.toPath(), new SimpleFileVisitor<Path>() { + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + String zipPath = sourceDir.toPath().relativize(file).toString(); + if (skipFiles.stream().anyMatch(zipPath::endsWith)) { + return FileVisitResult.CONTINUE; + } + + zos.putNextEntry(new ZipEntry(zipPath)); + Files.copy(file, zos); + filesAdded.add(zipPath); + return FileVisitResult.CONTINUE; + } + }); + + return filesAdded; + } + + public static void extractFile(String srcZip, String targetFile, String outputFilePath) + throws IOException { + try (OutputStream out = new FileOutputStream(outputFilePath); + ZipInputStream zis = new ZipInputStream(new FileInputStream(srcZip));) { + ZipEntry ze = zis.getNextEntry(); + while (ze != null) { + if (ze.getName().equals(targetFile)) { + byte[] buf = new byte[1024]; + int read = 0; + + while ((read = zis.read(buf)) > -1) { + out.write(buf, 0, read); + } + + out.close(); + break; + } + + ze = zis.getNextEntry(); + } + } + } +} |