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