/*
* 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.
*
*
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}.
*
*
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 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 argsToFilter =
Stream.of("--asan", "--ubsan", "--hwasan", "--native").collect(toSet());
ProcessBuilder processBuilder = new ProcessBuilder();
List 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 additionalEnvironment = new HashMap<>();
additionalEnvironment.put(preloadVariable(),
appendWithPathListSeparator(
preloadVariable(), preloadLibs.stream().map(Path::toString).toArray(String[] ::new)));
List 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 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 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 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 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 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 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 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 candidateNames) {
for (String name : candidateNames) {
Optional path = tryFindLibraryInJazzerNativeSanitizersDir(name);
if (path.isPresent()) {
return path.get();
}
}
for (String name : candidateNames) {
Optional 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 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 tryFindLibraryUsingClang(String name) {
List 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 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 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 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 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);
}
}
}