diff options
Diffstat (limited to 'src/main/java/com/code_intelligence/jazzer/Jazzer.java')
-rw-r--r-- | src/main/java/com/code_intelligence/jazzer/Jazzer.java | 515 |
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); + } + } +} |