aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/code_intelligence/jazzer/Jazzer.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/code_intelligence/jazzer/Jazzer.java')
-rw-r--r--src/main/java/com/code_intelligence/jazzer/Jazzer.java515
1 files changed, 515 insertions, 0 deletions
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);
+ }
+ }
+}