xref: /aosp_15_r20/external/jazzer-api/src/main/java/com/code_intelligence/jazzer/Jazzer.java (revision 33edd6723662ea34453766bfdca85dbfdd5342b8)
1 /*
2  * Copyright 2022 Code Intelligence GmbH
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.code_intelligence.jazzer;
18 
19 import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID;
20 import static java.lang.System.exit;
21 import static java.util.Arrays.asList;
22 import static java.util.Collections.singletonList;
23 import static java.util.stream.Collectors.joining;
24 import static java.util.stream.Collectors.toList;
25 import static java.util.stream.Collectors.toSet;
26 
27 import com.code_intelligence.jazzer.android.AndroidRuntime;
28 import com.code_intelligence.jazzer.driver.Driver;
29 import com.code_intelligence.jazzer.utils.Log;
30 import com.code_intelligence.jazzer.utils.ZipUtils;
31 import com.github.fmeum.rules_jni.RulesJni;
32 import java.io.ByteArrayOutputStream;
33 import java.io.File;
34 import java.io.IOException;
35 import java.io.InputStream;
36 import java.io.OutputStream;
37 import java.lang.management.ManagementFactory;
38 import java.nio.charset.StandardCharsets;
39 import java.nio.file.FileSystems;
40 import java.nio.file.Files;
41 import java.nio.file.Path;
42 import java.nio.file.Paths;
43 import java.nio.file.attribute.FileAttribute;
44 import java.nio.file.attribute.PosixFilePermissions;
45 import java.util.AbstractMap.SimpleEntry;
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.HashMap;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Optional;
52 import java.util.Set;
53 import java.util.stream.Stream;
54 
55 /**
56  * The libFuzzer-compatible CLI entrypoint for Jazzer.
57  *
58  * <p>Arguments to Jazzer are passed as command-line arguments or {@code jazzer.*} system
59  * properties. For example, setting the property {@code jazzer.target_class} to
60  * {@code com.example.FuzzTest} is equivalent to passing the argument
61  * {@code --target_class=com.example.FuzzTest}.
62  *
63  * <p>Arguments to libFuzzer are passed as command-line arguments.
64  */
65 public class Jazzer {
main(String[] args)66   public static void main(String[] args) throws IOException, InterruptedException {
67     start(Arrays.stream(args).collect(toList()));
68   }
69 
70   // Accessed by jazzer_main.cpp.
71   @SuppressWarnings("unused")
main(byte[][] nativeArgs)72   private static void main(byte[][] nativeArgs) throws IOException, InterruptedException {
73     start(Arrays.stream(nativeArgs)
74               .map(bytes -> new String(bytes, StandardCharsets.UTF_8))
75               .collect(toList()));
76   }
77 
start(List<String> args)78   private static void start(List<String> args) throws IOException, InterruptedException {
79     // Lock in the output PrintStreams so that Jazzer can still emit output even if the fuzz target
80     // itself is "silenced" by redirecting System.out and/or System.err.
81     Log.fixOutErr(System.out, System.err);
82 
83     parseJazzerArgsToProperties(args);
84 
85     // --asan and --ubsan imply --native by default, but --native can also be used by itself to fuzz
86     // native libraries without sanitizers (e.g. to quickly grow a corpus).
87     final boolean loadASan = Boolean.parseBoolean(System.getProperty("jazzer.asan", "false"));
88     final boolean loadUBSan = Boolean.parseBoolean(System.getProperty("jazzer.ubsan", "false"));
89     final boolean loadHWASan = Boolean.parseBoolean(System.getProperty("jazzer.hwasan", "false"));
90     final boolean fuzzNative = Boolean.parseBoolean(
91         System.getProperty("jazzer.native", Boolean.toString(loadASan || loadUBSan || loadHWASan)));
92     if ((loadASan || loadUBSan || loadHWASan) && !fuzzNative) {
93       Log.error("--asan, --hwasan and --ubsan cannot be used without --native");
94       exit(1);
95     }
96     // No native fuzzing has been requested, fuzz in the current process.
97     if (!fuzzNative) {
98       if (IS_ANDROID) {
99         final String initOptions = getAndroidRuntimeOptions();
100         AndroidRuntime.initialize(initOptions);
101       }
102       // We only create a wrapper script if libFuzzer runs in a mode that creates subprocesses.
103       // In LibFuzzer's fork mode, the subprocesses created continuously by the main libFuzzer
104       // process do not create further subprocesses. Creating a wrapper script for each subprocess
105       // is an unnecessary overhead.
106       final boolean spawnsSubprocesses = args.stream().anyMatch(arg
107           -> (arg.startsWith("-fork=") && !arg.equals("-fork=0"))
108               || (arg.startsWith("-jobs=") && !arg.equals("-jobs=0"))
109               || (arg.startsWith("-merge=") && !arg.equals("-merge=0")));
110       // argv0 is printed by libFuzzer during reproduction, so have it contain "jazzer".
111       String arg0 = spawnsSubprocesses ? prepareArgv0(new HashMap<>()) : "jazzer";
112       args = Stream.concat(Stream.of(arg0), args.stream()).collect(toList());
113       exit(Driver.start(args, spawnsSubprocesses));
114     }
115 
116     if (!isLinux() && !isMacOs()) {
117       Log.error("--asan, --ubsan, and --native are only supported on Linux and macOS");
118       exit(1);
119     }
120 
121     // Run ourselves as a subprocess with `jazzer_preload` and (optionally) native sanitizers
122     // preloaded. By inheriting IO, this wrapping should become invisible for the user.
123     Set<String> argsToFilter =
124         Stream.of("--asan", "--ubsan", "--hwasan", "--native").collect(toSet());
125     ProcessBuilder processBuilder = new ProcessBuilder();
126     List<Path> preloadLibs = new ArrayList<>();
127     // We have to load jazzer_preload before we load ASan since the ASan includes no-op definitions
128     // of the fuzzer callbacks as weak symbols, but the dynamic linker doesn't distinguish between
129     // strong and weak symbols.
130     preloadLibs.add(RulesJni.extractLibrary("jazzer_preload", Jazzer.class));
131     if (loadASan) {
132       processBuilder.environment().compute("ASAN_OPTIONS",
133           (name, currentValue)
134               -> appendWithPathListSeparator(name,
135                   // The JVM produces an extremely large number of false positive leaks, which makes
136                   // it impossible to use LeakSanitizer.
137                   // TODO: Investigate whether we can hook malloc/free only for JNI shared
138                   // libraries, not the JVM itself.
139                   "detect_leaks=0",
140                   // We load jazzer_preload first.
141                   "verify_asan_link_order=0"));
142       Log.warn("Jazzer is not compatible with LeakSanitizer. Leaks are not reported.");
143       preloadLibs.add(findLibrary(asanLibNames()));
144     }
145     if (loadHWASan) {
146       processBuilder.environment().compute("HWASAN_OPTIONS",
147           (name, currentValue)
148               -> appendWithPathListSeparator(name,
149                   // The JVM produces an extremely large number of false positive leaks, which makes
150                   // it impossible to use LeakSanitizer.
151                   // TODO: Investigate whether we can hook malloc/free only for JNI shared
152                   // libraries, not the JVM itself.
153                   "detect_leaks=0",
154                   // We load jazzer_preload first.
155                   "verify_asan_link_order=0"));
156       Log.warn("Jazzer is not compatible with LeakSanitizer. Leaks are not reported.");
157       preloadLibs.add(findLibrary(hwasanLibNames()));
158     }
159     if (loadUBSan) {
160       preloadLibs.add(findLibrary(ubsanLibNames()));
161     }
162     // The launcher script we generate is executed by /bin/sh on macOS, which is codesigned without
163     // the allow-dyld-environment-variables entitlement. The dynamic linker would thus remove all
164     // DYLD_* variables. Instead, we pass these variables directly to the java executable by
165     // emitting them into the wrapper. The java binary has both the allow-dyld-environment-variables
166     // and the disable-library-validation entitlement, which allows any codesigned library to be
167     // preloaded.
168     processBuilder.environment().remove(preloadVariable());
169     Map<String, String> additionalEnvironment = new HashMap<>();
170     additionalEnvironment.put(preloadVariable(),
171         appendWithPathListSeparator(
172             preloadVariable(), preloadLibs.stream().map(Path::toString).toArray(String[] ::new)));
173     List<String> subProcessArgs =
174         Stream
175             .concat(Stream.of(prepareArgv0(additionalEnvironment)),
176                 // Prevent a "fork bomb" by stripping all args that trigger this code path.
177                 args.stream().filter(arg -> !argsToFilter.contains(arg.split("=")[0])))
178             .collect(toList());
179     processBuilder.command(subProcessArgs);
180     processBuilder.inheritIO();
181 
182     exit(processBuilder.start().waitFor());
183   }
184 
parseJazzerArgsToProperties(List<String> args)185   private static void parseJazzerArgsToProperties(List<String> args) {
186     args.stream()
187         .filter(arg -> arg.startsWith("--"))
188         .map(arg -> arg.substring("--".length()))
189         // Filter out "--", which can be used to declare that all further arguments aren't libFuzzer
190         // arguments.
191         .filter(arg -> !arg.isEmpty())
192         .map(Jazzer::parseSingleArg)
193         .forEach(e -> System.setProperty("jazzer." + e.getKey(), e.getValue()));
194   }
195 
parseSingleArg(String arg)196   private static SimpleEntry<String, String> parseSingleArg(String arg) {
197     String[] nameAndValue = arg.split("=", 2);
198     if (nameAndValue.length == 2) {
199       // Example: --keep_going=10 --> (keep_going, 10)
200       return new SimpleEntry<>(nameAndValue[0], nameAndValue[1]);
201     } else if (nameAndValue[0].startsWith("no")) {
202       // Example: --nohooks --> (hooks, "false")
203       return new SimpleEntry<>(nameAndValue[0].substring("no".length()), "false");
204     } else {
205       // Example: --dedup --> (dedup, "true")
206       return new SimpleEntry<>(nameAndValue[0], "true");
207     }
208   }
209 
210   // Create a wrapper script that faithfully recreates the current JVM. By using this script as
211   // libFuzzer's argv[0], libFuzzer modes that rely on subprocesses can work with the Java driver.
212   // This trick is also used to allow native sanitizers to be preloaded.
prepareArgv0(Map<String, String> additionalEnvironment)213   private static String prepareArgv0(Map<String, String> additionalEnvironment) throws IOException {
214     if (!isPosixOrAndroid() && !additionalEnvironment.isEmpty()) {
215       throw new IllegalArgumentException(
216           "Setting environment variables in the wrapper is only supported on POSIX systems and Android");
217     }
218     char shellQuote = isPosixOrAndroid() ? '\'' : '"';
219     String launcherTemplate;
220     if (IS_ANDROID) {
221       launcherTemplate = "#!/system/bin/env sh\n%s LD_LIBRARY_PATH=%s \n%s $@\n";
222     } else if (isPosix()) {
223       launcherTemplate = "#!/usr/bin/env sh\n%s $@\n";
224     } else {
225       launcherTemplate = "@echo off\r\n%s %%*\r\n";
226     }
227 
228     String launcherExtension = isPosix() ? ".sh" : ".bat";
229     FileAttribute<?>[] launcherScriptAttributes = isPosixOrAndroid()
230         ? new FileAttribute[] {PosixFilePermissions.asFileAttribute(
231             PosixFilePermissions.fromString("rwx------"))}
232         : new FileAttribute[] {};
233     String env = additionalEnvironment.entrySet()
234                      .stream()
235                      .map(e -> e.getKey() + "='" + e.getValue() + "'")
236                      .collect(joining(" "));
237     String command =
238         Stream
239             .concat(Stream.of(IS_ANDROID ? "exec" : javaBinary().toString()), javaBinaryArgs())
240             // Escape individual arguments for the shell.
241             .map(str -> shellQuote + str + shellQuote)
242             .collect(joining(" "));
243 
244     String invocation = env.isEmpty() ? command : env + " " + command;
245 
246     // argv0 is printed by libFuzzer during reproduction, so have the launcher basename contain
247     // "jazzer".
248     Path launcher;
249     String launcherContent;
250     if (IS_ANDROID) {
251       String exportCommand = AndroidRuntime.getClassPathsCommand();
252       String ldLibraryPath = AndroidRuntime.getLdLibraryPath();
253       launcherContent = String.format(launcherTemplate, exportCommand, ldLibraryPath, invocation);
254       launcher = Files.createTempFile(
255           Paths.get("/data/local/tmp/"), "jazzer-", launcherExtension, launcherScriptAttributes);
256     } else {
257       launcherContent = String.format(launcherTemplate, invocation);
258       launcher = Files.createTempFile("jazzer-", launcherExtension, launcherScriptAttributes);
259     }
260 
261     launcher.toFile().deleteOnExit();
262     Files.write(launcher, launcherContent.getBytes(StandardCharsets.UTF_8));
263     return launcher.toAbsolutePath().toString();
264   }
265 
javaBinary()266   private static Path javaBinary() {
267     String javaBinaryName;
268     if (isPosix()) {
269       javaBinaryName = "java";
270     } else {
271       javaBinaryName = "java.exe";
272     }
273 
274     return Paths.get(System.getProperty("java.home"), "bin", javaBinaryName);
275   }
276 
javaBinaryArgs()277   private static Stream<String> javaBinaryArgs() throws IOException {
278     if (IS_ANDROID) {
279       // Add Android specific args
280       Path agentPath =
281           RulesJni.extractLibrary("android_native_agent", "/com/code_intelligence/jazzer/android");
282 
283       String jazzerAgentPath = System.getProperty("jazzer.agent_path");
284       String bootclassClassOverrides =
285           System.getProperty("jazzer.android_bootpath_classes_overrides");
286 
287       String jazzerBootstrapJarPath =
288           "com/code_intelligence/jazzer/android/jazzer_bootstrap_android.jar";
289       String jazzerBootstrapJarOut = "/data/local/tmp/jazzer_bootstrap_android.jar";
290 
291       try {
292         ZipUtils.extractFile(jazzerAgentPath, jazzerBootstrapJarPath, jazzerBootstrapJarOut);
293       } catch (IOException ioe) {
294         Log.error(
295             "Could not extract jazzer_bootstrap_android.jar from Jazzer standalone agent", ioe);
296         exit(1);
297       }
298 
299       String nativeAgentOptions = "injectJars=" + jazzerBootstrapJarOut;
300       if (bootclassClassOverrides != null && !bootclassClassOverrides.isEmpty()) {
301         nativeAgentOptions += ",bootstrapClassOverrides=" + bootclassClassOverrides;
302       }
303 
304       // ManagementFactory wont work with Android
305       Stream<String> stream = Stream.of("app_process", "-Djdk.attach.allowAttachSelf=true",
306           "-Xplugin:libopenjdkjvmti.so",
307           "-agentpath:" + agentPath.toString() + "=" + nativeAgentOptions, "-Xcompiler-option",
308           "--debuggable", "/system/bin", Jazzer.class.getName());
309 
310       return stream;
311     }
312 
313     Stream<String> stream = Stream.of("-cp", System.getProperty("java.class.path"),
314         // Make ByteBuddyAgent's job simpler by allowing it to attach directly to the JVM
315         // rather than relying on an external helper. The latter fails on macOS 12 with JDK 11+
316         // (but not 8) and UBSan preloaded with:
317         // Caused by: java.io.IOException: Cannot run program
318         // "/Users/runner/hostedtoolcache/Java_Zulu_jdk/17.0.4-8/x64/bin/java": error=0, Failed
319         // to exec spawn helper: pid: 8227, signal: 9
320         // Presumably, this issue is caused by codesigning and the exec helper missing the
321         // entitlements required for library insertion.
322         "-Djdk.attach.allowAttachSelf=true", Jazzer.class.getName());
323 
324     return Stream.concat(ManagementFactory.getRuntimeMXBean().getInputArguments().stream(), stream);
325   }
326 
327   /**
328    * Append the given elements to the value of the environment variable {@code name} that contains a
329    * list of paths separated by the system path list separator.
330    */
appendWithPathListSeparator(String name, String... options)331   private static String appendWithPathListSeparator(String name, String... options) {
332     if (options.length == 0) {
333       throw new IllegalArgumentException("options must not be empty");
334     }
335 
336     String currentValue = Optional.ofNullable(System.getenv(name)).orElse("");
337     String additionalOptions = String.join(File.pathSeparator, options);
338     if (currentValue.isEmpty()) {
339       return additionalOptions;
340     }
341     return currentValue + File.pathSeparator + additionalOptions;
342   }
343 
findLibrary(List<String> candidateNames)344   private static Path findLibrary(List<String> candidateNames) {
345     if (!IS_ANDROID) {
346       return findHostClangLibrary(candidateNames);
347     }
348 
349     for (String candidateName : candidateNames) {
350       String candidateFullPath = "/apex/com.android.runtime/lib64/bionic/" + candidateName;
351       File f = new File(candidateFullPath);
352       if (f.exists()) {
353         return Paths.get(candidateFullPath);
354       }
355     }
356 
357     Log.error(
358         String.format("Failed to find one of %s%n for Android", String.join(", ", candidateNames)));
359     Log.error("If fuzzing hwasan, make sure you have a hwasan build flashed to your device");
360 
361     exit(1);
362     throw new IllegalStateException("not reached");
363   }
364 
findHostClangLibrary(List<String> candidateNames)365   private static Path findHostClangLibrary(List<String> candidateNames) {
366     for (String name : candidateNames) {
367       Optional<Path> path = tryFindLibraryInJazzerNativeSanitizersDir(name);
368       if (path.isPresent()) {
369         return path.get();
370       }
371     }
372     for (String name : candidateNames) {
373       Optional<Path> path = tryFindLibraryUsingClang(name);
374       if (path.isPresent()) {
375         return path.get();
376       }
377     }
378     Log.error("Failed to find one of: " + String.join(", ", candidateNames));
379     exit(1);
380     throw new IllegalStateException("not reached");
381   }
382 
tryFindLibraryInJazzerNativeSanitizersDir(String name)383   private static Optional<Path> tryFindLibraryInJazzerNativeSanitizersDir(String name) {
384     String nativeSanitizersDir = System.getenv("JAZZER_NATIVE_SANITIZERS_DIR");
385     if (nativeSanitizersDir == null) {
386       return Optional.empty();
387     }
388     Path candidatePath = Paths.get(nativeSanitizersDir, name);
389     if (Files.exists(candidatePath)) {
390       return Optional.of(candidatePath);
391     } else {
392       return Optional.empty();
393     }
394   }
395 
396   /**
397    * Given a library name such as "libclang_rt.asan-x86_64.so", get the full path to the library
398    * installed on the host from clang (or CC, if set). Returns Optional.empty() if clang does not
399    * find the library and exits with a message in case of any other error condition.
400    */
tryFindLibraryUsingClang(String name)401   private static Optional<Path> tryFindLibraryUsingClang(String name) {
402     List<String> command = asList(hostClang(), "--print-file-name", name);
403     ProcessBuilder processBuilder = new ProcessBuilder(command);
404     byte[] output;
405     try {
406       Process process = processBuilder.start();
407       if (process.waitFor() != 0) {
408         Log.error(String.format(
409             "'%s' exited with exit code %d", String.join(" ", command), process.exitValue()));
410         copy(process.getInputStream(), System.out);
411         copy(process.getErrorStream(), System.err);
412         exit(1);
413       }
414       output = readAllBytes(process.getInputStream());
415     } catch (IOException | InterruptedException e) {
416       Log.error(String.format("Failed to run '%s'", String.join(" ", command)), e);
417       exit(1);
418       throw new IllegalStateException("not reached");
419     }
420     Path library = Paths.get(new String(output).trim());
421     if (Files.exists(library)) {
422       return Optional.of(library);
423     }
424     return Optional.empty();
425   }
426 
hostClang()427   private static String hostClang() {
428     return Optional.ofNullable(System.getenv("CC")).orElse("clang");
429   }
430 
hwasanLibNames()431   private static List<String> hwasanLibNames() {
432     if (!IS_ANDROID) {
433       Log.error("HWAsan is only supported for Android. Please try --asan");
434       exit(1);
435     }
436 
437     return singletonList("libclang_rt.hwasan-aarch64-android.so");
438   }
439 
asanLibNames()440   private static List<String> asanLibNames() {
441     if (isLinux()) {
442       if (IS_ANDROID) {
443         Log.error("ASan is not supported for Android at this time. Use --hwasan for Address "
444             + "Sanitization on Android");
445         exit(1);
446       }
447 
448       // Since LLVM 15 sanitizer runtimes no longer have the architecture in the filename.
449       return asList("libclang_rt.asan.so", "libclang_rt.asan-x86_64.so");
450     } else {
451       return singletonList("libclang_rt.asan_osx_dynamic.dylib");
452     }
453   }
454 
ubsanLibNames()455   private static List<String> ubsanLibNames() {
456     if (isLinux()) {
457       if (IS_ANDROID) {
458         // return asList("libclang_rt.ubsan_standalone-aarch64-android.so");
459         Log.error("ERROR: UBSan is not supported for Android at this time.");
460         exit(1);
461       }
462 
463       return asList("libclang_rt.ubsan_standalone.so", "libclang_rt.ubsan_standalone-x86_64.so");
464     } else {
465       return singletonList("libclang_rt.ubsan_osx_dynamic.dylib");
466     }
467   }
468 
preloadVariable()469   private static String preloadVariable() {
470     return isLinux() ? "LD_PRELOAD" : "DYLD_INSERT_LIBRARIES";
471   }
472 
isLinux()473   private static boolean isLinux() {
474     return System.getProperty("os.name").startsWith("Linux");
475   }
476 
isMacOs()477   private static boolean isMacOs() {
478     return System.getProperty("os.name").startsWith("Mac OS X");
479   }
480 
isPosix()481   private static boolean isPosix() {
482     return !IS_ANDROID && FileSystems.getDefault().supportedFileAttributeViews().contains("posix");
483   }
484 
getAndroidRuntimeOptions()485   private static String getAndroidRuntimeOptions() {
486     List<String> validInitOptions = Arrays.asList("use_platform_libs", "use_none", "");
487     String initOptString = System.getProperty("jazzer.android_init_options");
488     if (!validInitOptions.contains(initOptString)) {
489       Log.error("Invalid android_init_options set for Android Runtime.");
490       exit(1);
491     }
492     return initOptString;
493   }
494 
isPosixOrAndroid()495   private static boolean isPosixOrAndroid() {
496     if (isPosix()) {
497       return true;
498     }
499     return IS_ANDROID;
500   }
501 
readAllBytes(InputStream in)502   private static byte[] readAllBytes(InputStream in) throws IOException {
503     ByteArrayOutputStream out = new ByteArrayOutputStream();
504     copy(in, out);
505     return out.toByteArray();
506   }
507 
copy(InputStream source, OutputStream target)508   private static void copy(InputStream source, OutputStream target) throws IOException {
509     byte[] buffer = new byte[64 * 104 * 1024];
510     int read;
511     while ((read = source.read(buffer)) != -1) {
512       target.write(buffer, 0, read);
513     }
514   }
515 }
516