1 /* 2 * Copyright 2023 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.driver; 18 19 import static com.code_intelligence.jazzer.driver.Constants.JAZZER_FINDING_EXIT_CODE; 20 import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID; 21 import static java.lang.System.exit; 22 import static java.util.stream.Collectors.joining; 23 24 import com.code_intelligence.jazzer.api.FuzzedDataProvider; 25 import com.code_intelligence.jazzer.autofuzz.FuzzTarget; 26 import com.code_intelligence.jazzer.instrumentor.CoverageRecorder; 27 import com.code_intelligence.jazzer.mutation.ArgumentsMutator; 28 import com.code_intelligence.jazzer.runtime.FuzzTargetRunnerNatives; 29 import com.code_intelligence.jazzer.runtime.JazzerInternal; 30 import com.code_intelligence.jazzer.utils.Log; 31 import com.code_intelligence.jazzer.utils.UnsafeProvider; 32 import java.io.ByteArrayInputStream; 33 import java.io.ByteArrayOutputStream; 34 import java.io.IOException; 35 import java.lang.invoke.MethodHandle; 36 import java.lang.invoke.MethodHandles; 37 import java.lang.reflect.InvocationTargetException; 38 import java.lang.reflect.Method; 39 import java.lang.reflect.Modifier; 40 import java.math.BigInteger; 41 import java.nio.charset.StandardCharsets; 42 import java.security.MessageDigest; 43 import java.security.NoSuchAlgorithmException; 44 import java.util.ArrayList; 45 import java.util.Base64; 46 import java.util.Collections; 47 import java.util.HashSet; 48 import java.util.List; 49 import java.util.Locale; 50 import java.util.Set; 51 import java.util.function.Predicate; 52 import java.util.stream.Stream; 53 import sun.misc.Unsafe; 54 55 /** 56 * Executes a fuzz target and reports findings. 57 * 58 * <p>This class maintains global state (both native and non-native) and thus cannot be used 59 * concurrently. 60 */ 61 public final class FuzzTargetRunner { 62 private static final String OPENTEST4J_TEST_ABORTED_EXCEPTION = 63 "org.opentest4j.TestAbortedException"; 64 65 private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe(); 66 67 private static final long BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class); 68 69 // Possible return values for the libFuzzer callback runOne. 70 private static final int LIBFUZZER_CONTINUE = 0; 71 private static final int LIBFUZZER_RETURN_FROM_DRIVER = -2; 72 73 private static boolean invalidCorpusFileWarningShown = false; 74 private static final Set<Long> ignoredTokens = new HashSet<>(Opt.ignore); 75 private static final FuzzedDataProviderImpl fuzzedDataProvider = 76 FuzzedDataProviderImpl.withNativeData(); 77 private static final MethodHandle fuzzTargetMethod; 78 private static final boolean useFuzzedDataProvider; 79 // Reused in every iteration analogous to JUnit's PER_CLASS lifecycle. 80 private static final Object fuzzTargetInstance; 81 private static final Method fuzzerTearDown; 82 private static final ArgumentsMutator mutator; 83 private static final ReproducerTemplate reproducerTemplate; 84 private static Predicate<Throwable> findingHandler; 85 86 static { 87 FuzzTargetHolder.FuzzTarget fuzzTarget = FuzzTargetHolder.fuzzTarget; 88 Class<?> fuzzTargetClass = fuzzTarget.method.getDeclaringClass(); 89 90 // The method may not be accessible - JUnit test classes and methods are usually declared 91 // without access modifiers and thus package-private. 92 fuzzTarget.method.setAccessible(true); 93 try { 94 fuzzTargetMethod = MethodHandles.lookup().unreflect(fuzzTarget.method); 95 } catch (IllegalAccessException e) { 96 throw new IllegalStateException(e); 97 } 98 useFuzzedDataProvider = fuzzTarget.usesFuzzedDataProvider(); 99 if (!useFuzzedDataProvider && IS_ANDROID) { 100 Log.error("Android fuzz targets must use " + FuzzedDataProvider.class.getName()); 101 exit(1); 102 throw new IllegalStateException("Not reached"); 103 } 104 105 fuzzerTearDown = fuzzTarget.tearDown.orElse(null); 106 reproducerTemplate = new ReproducerTemplate(fuzzTargetClass.getName(), useFuzzedDataProvider); 107 fuzzTargetClass.getName()108 JazzerInternal.onFuzzTargetReady(fuzzTargetClass.getName()); 109 110 try { 111 fuzzTargetInstance = fuzzTarget.newInstance.call(); 112 } catch (Throwable t) { 113 Log.finding(t); 114 exit(1); 115 throw new IllegalStateException("Not reached"); 116 } 117 118 if (Opt.experimentalMutator) { 119 if (Modifier.isStatic(fuzzTarget.method.getModifiers())) { 120 mutator = ArgumentsMutator.forStaticMethodOrThrow(fuzzTarget.method); 121 } else { 122 mutator = ArgumentsMutator.forInstanceMethodOrThrow(fuzzTargetInstance, fuzzTarget.method); 123 } 124 Log.info("Using experimental mutator: " + mutator); 125 } else { 126 mutator = null; 127 } 128 129 if (Opt.hooks) { 130 // libFuzzer will clear the coverage map after this method returns and keeps no record of the 131 // coverage accumulated so far (e.g. by static initializers). We record it here to keep it 132 // around for JaCoCo coverage reports. CoverageRecorder.updateCoveredIdsWithCoverageMap()133 CoverageRecorder.updateCoveredIdsWithCoverageMap(); 134 } 135 136 Runtime.getRuntime().addShutdownHook(new Thread(FuzzTargetRunner::shutdown)); 137 } 138 139 /** 140 * A test-only convenience wrapper around {@link #runOne(long, int)}. 141 */ runOne(byte[] data)142 static int runOne(byte[] data) { 143 long dataPtr = UNSAFE.allocateMemory(data.length); 144 UNSAFE.copyMemory(data, BYTE_ARRAY_OFFSET, null, dataPtr, data.length); 145 try { 146 return runOne(dataPtr, data.length); 147 } finally { 148 UNSAFE.freeMemory(dataPtr); 149 } 150 } 151 152 /** 153 * Executes the user-provided fuzz target once. 154 * 155 * @param dataPtr a native pointer to beginning of the input provided by the fuzzer for this 156 * execution 157 * @param dataLength length of the fuzzer input 158 * @return the value that the native LLVMFuzzerTestOneInput function should return. Currently, 159 * this is always 0. The function may exit the process instead of returning. 160 */ runOne(long dataPtr, int dataLength)161 private static int runOne(long dataPtr, int dataLength) { 162 Throwable finding = null; 163 byte[] data; 164 Object argument; 165 if (Opt.experimentalMutator) { 166 // TODO: Instead of copying the native data and then reading it in, consider the following 167 // optimizations if they turn out to be worthwhile in benchmarks: 168 // 1. Let libFuzzer pass in a null pointer if the byte array hasn't changed since the last 169 // call to our custom mutator and skip the read entirely. 170 // 2. Implement a InputStream backed by Unsafe to avoid the copyToArray overhead. 171 byte[] buf = copyToArray(dataPtr, dataLength); 172 boolean readExactly = mutator.read(new ByteArrayInputStream(buf)); 173 174 // All inputs constructed by the mutator framework can be read exactly, existing corpus files 175 // may not be valid for the current fuzz target anymore, though. In this case, print a warning 176 // once. 177 if (!(invalidCorpusFileWarningShown || readExactly || isFixedLibFuzzerInput(buf))) { 178 invalidCorpusFileWarningShown = true; 179 Log.warn("Some files in the seed corpus do not match the fuzz target signature. " 180 + "This indicates that they were generated with a different signature and may cause issues reproducing previous findings."); 181 } 182 data = null; 183 argument = null; 184 } else if (useFuzzedDataProvider) { 185 fuzzedDataProvider.setNativeData(dataPtr, dataLength); 186 data = null; 187 argument = fuzzedDataProvider; 188 } else { 189 data = copyToArray(dataPtr, dataLength); 190 argument = data; 191 } 192 try { 193 if (Opt.experimentalMutator) { 194 // No need to detach as we are currently reading in the mutator state from bytes in every 195 // iteration. 196 mutator.invoke(false); 197 } else if (fuzzTargetInstance == null) { 198 fuzzTargetMethod.invoke(argument); 199 } else { 200 fuzzTargetMethod.invoke(fuzzTargetInstance, argument); 201 } 202 } catch (Throwable uncaughtFinding) { 203 finding = uncaughtFinding; 204 } 205 206 // When using libFuzzer's -merge flag, only the coverage of the current input is relevant, not 207 // whether it is crashing. Since every crash would cause a restart of the process and thus the 208 // JVM, we can optimize this case by not crashing. 209 // 210 // Incidentally, this makes the behavior of fuzz targets relying on global states more 211 // consistent: Rather than resetting the global state after every crashing input and thus 212 // dependent on the particular ordering of the inputs, we never reset it. 213 if (Opt.mergeInner) { 214 return LIBFUZZER_CONTINUE; 215 } 216 217 // Explicitly reported findings take precedence over uncaught exceptions. 218 if (JazzerInternal.lastFinding != null) { 219 finding = JazzerInternal.lastFinding; 220 JazzerInternal.lastFinding = null; 221 } 222 // Allow skipping invalid inputs in fuzz tests by using e.g. JUnit's assumeTrue. 223 if (finding == null || finding.getClass().getName().equals(OPENTEST4J_TEST_ABORTED_EXCEPTION)) { 224 return LIBFUZZER_CONTINUE; 225 } 226 if (Opt.hooks) { 227 finding = ExceptionUtils.preprocessThrowable(finding); 228 } 229 230 long dedupToken = Opt.dedup ? ExceptionUtils.computeDedupToken(finding) : 0; 231 if (Opt.dedup && !ignoredTokens.add(dedupToken)) { 232 return LIBFUZZER_CONTINUE; 233 } 234 235 if (findingHandler != null) { 236 // We still print the libFuzzer crashing input information, which also dumps the crashing 237 // input as a side effect. 238 printCrashingInput(); 239 if (findingHandler.test(finding)) { 240 return LIBFUZZER_CONTINUE; 241 } else { 242 return LIBFUZZER_RETURN_FROM_DRIVER; 243 } 244 } 245 246 // The user-provided fuzz target method has returned. Any further exits are on us and should not 247 // result in a "fuzz target exited" warning being printed by libFuzzer. 248 temporarilyDisableLibfuzzerExitHook(); 249 250 Log.finding(finding); 251 if (Opt.dedup) { 252 // Has to be printed to stdout as it is parsed by libFuzzer when minimizing a crash. It does 253 // not necessarily have to appear at the beginning of a line. 254 // https://github.com/llvm/llvm-project/blob/4c106c93eb68f8f9f201202677cd31e326c16823/compiler-rt/lib/fuzzer/FuzzerDriver.cpp#L342 255 Log.structuredOutput(String.format(Locale.ROOT, "DEDUP_TOKEN: %016x", dedupToken)); 256 } 257 Log.println("== libFuzzer crashing input =="); 258 printCrashingInput(); 259 // dumpReproducer needs to be called after libFuzzer printed its final stats as otherwise it 260 // would report incorrect coverage - the reproducer generation involved rerunning the fuzz 261 // target. 262 // It doesn't support @FuzzTest fuzz targets, but these come with an integrated regression test 263 // that satisfies the same purpose. 264 // It also doesn't support the experimental mutator yet as that requires implementing Java code 265 // generation for mutators. 266 if (fuzzTargetInstance == null && !Opt.experimentalMutator) { 267 dumpReproducer(data); 268 } 269 270 if (!Opt.dedup || Long.compareUnsigned(ignoredTokens.size(), Opt.keepGoing) >= 0) { 271 // Reached the maximum amount of findings to keep going for, crash after shutdown. We use 272 // _Exit rather than System.exit to not trigger libFuzzer's exit handlers. 273 if (!Opt.autofuzz.isEmpty() && Opt.dedup) { 274 Log.println(""); 275 Log.info(String.format( 276 "To continue fuzzing past this particular finding, rerun with the following additional argument:" 277 + "%n%n --ignore=%s%n%n" 278 + "To ignore all findings of this kind, rerun with the following additional argument:" 279 + "%n%n --autofuzz_ignore=%s", 280 ignoredTokens.stream() 281 .map(token -> Long.toUnsignedString(token, 16)) 282 .collect(joining(",")), 283 Stream.concat(Opt.autofuzzIgnore.stream(), Stream.of(finding.getClass().getName())) 284 .collect(joining(",")))); 285 } 286 System.exit(JAZZER_FINDING_EXIT_CODE); 287 throw new IllegalStateException("Not reached"); 288 } 289 return LIBFUZZER_CONTINUE; 290 } 291 isFixedLibFuzzerInput(byte[] input)292 private static boolean isFixedLibFuzzerInput(byte[] input) { 293 // Detect special libFuzzer inputs which can not be processed by the mutator framework. 294 // libFuzzer always uses an empty input, and one with a single line feed (10) to indicate 295 // end of initial corpus file processing. 296 return input.length == 0 || (input.length == 1 && input[0] == 10); 297 } 298 299 // Called via JNI, being passed data from LLVMFuzzerCustomMutator. 300 @SuppressWarnings("unused") mutateOne(long data, int size, int maxSize, int seed)301 private static int mutateOne(long data, int size, int maxSize, int seed) { 302 mutate(data, size, seed); 303 return writeToMemory(mutator, data, maxSize); 304 } 305 mutate(long data, int size, int seed)306 private static void mutate(long data, int size, int seed) { 307 // libFuzzer sends the input "\n" when there are no corpus entries. We use that as a signal to 308 // initialize the mutator instead of just reading that trivial input to produce a more 309 // interesting value. 310 if (size == 1 && UNSAFE.getByte(data) == '\n') { 311 mutator.init(seed); 312 } else { 313 // TODO: See the comment on earlier calls to read for potential optimizations. 314 mutator.read(new ByteArrayInputStream(copyToArray(data, size))); 315 mutator.mutate(seed); 316 } 317 } 318 319 private static long crossOverCount = 0; 320 321 // Called via JNI, being passed data from LLVMFuzzerCustomCrossOver. 322 @SuppressWarnings("unused") crossOver( long data1, int size1, long data2, int size2, long out, int maxOutSize, int seed)323 private static int crossOver( 324 long data1, int size1, long data2, int size2, long out, int maxOutSize, int seed) { 325 // Custom cross over and custom mutate are the only mutators registered in 326 // libFuzzer, hence cross over is picked as often as mutate, which is way 327 // too frequently. Without custom mutate, cross over would be picked from 328 // the list of default mutators, so ~1/12 of the time. This also seems too 329 // much and is reduced to a configurable frequency, default 1/100, here, 330 // mutate is used in the other cases. 331 if (Opt.experimentalCrossOverFrequency != 0 332 && crossOverCount++ % Opt.experimentalCrossOverFrequency == 0) { 333 mutator.crossOver(new ByteArrayInputStream(copyToArray(data1, size1)), 334 new ByteArrayInputStream(copyToArray(data2, size2)), seed); 335 } else { 336 mutate(data1, size1, seed); 337 } 338 return writeToMemory(mutator, out, maxOutSize); 339 } 340 341 @SuppressWarnings("SameParameterValue") writeToMemory(ArgumentsMutator mutator, long out, int maxOutSize)342 private static int writeToMemory(ArgumentsMutator mutator, long out, int maxOutSize) { 343 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 344 // TODO: Instead of writing to a byte array and then copying that array's contents into 345 // memory, consider introducing an OutputStream backed by Unsafe. 346 mutator.write(baos); 347 byte[] mutatedBytes = baos.toByteArray(); 348 int newSize = Math.min(mutatedBytes.length, maxOutSize); 349 UNSAFE.copyMemory(mutatedBytes, BYTE_ARRAY_OFFSET, null, out, newSize); 350 return newSize; 351 } 352 353 /* 354 * Starts libFuzzer via LLVMFuzzerRunDriver. 355 */ startLibFuzzer(List<String> args)356 public static int startLibFuzzer(List<String> args) { 357 // We always define LLVMFuzzerCustomMutator, but only use it when --experimental_mutator is 358 // specified. libFuzzer contains logic that disables --len_control when it finds the custom 359 // mutator symbol: 360 // https://github.com/llvm/llvm-project/blob/da3623de2411dd931913eb510e94fe846c929c24/compiler-rt/lib/fuzzer/FuzzerDriver.cpp#L202-L207 361 // We thus have to explicitly set --len_control to its default value when not using the new 362 // mutator. 363 // TODO: libFuzzer still emits a message about --len_control being disabled by default even if 364 // we override it via a flag. We may want to patch this out. 365 if (!Opt.experimentalMutator) { 366 // args may not be mutable. 367 args = new ArrayList<>(args); 368 // https://github.com/llvm/llvm-project/blob/da3623de2411dd931913eb510e94fe846c929c24/compiler-rt/lib/fuzzer/FuzzerFlags.def#L19 369 args.add("-len_control=100"); 370 } 371 372 for (String arg : args.subList(1, args.size())) { 373 if (!arg.startsWith("-")) { 374 Log.info("using inputs from: " + arg); 375 } 376 } 377 378 if (!IS_ANDROID) { 379 SignalHandler.initialize(); 380 } 381 return startLibFuzzer( 382 args.stream().map(str -> str.getBytes(StandardCharsets.UTF_8)).toArray(byte[][] ::new)); 383 } 384 385 /** 386 * Registers a custom handler for findings. 387 * 388 * @param findingHandler a consumer for the finding that returns true if the fuzzer should 389 * continue fuzzing and false if it should return from 390 * {@link FuzzTargetRunner#startLibFuzzer(List)}. 391 */ registerFindingHandler(Predicate<Throwable> findingHandler)392 public static void registerFindingHandler(Predicate<Throwable> findingHandler) { 393 FuzzTargetRunner.findingHandler = findingHandler; 394 } 395 shutdown()396 private static void shutdown() { 397 if (!Opt.coverageDump.isEmpty() || !Opt.coverageReport.isEmpty()) { 398 if (!Opt.coverageDump.isEmpty()) { 399 CoverageRecorder.dumpJacocoCoverage(Opt.coverageDump); 400 } 401 if (!Opt.coverageReport.isEmpty()) { 402 CoverageRecorder.dumpCoverageReport(Opt.coverageReport); 403 } 404 } 405 406 if (fuzzerTearDown == null) { 407 return; 408 } 409 Log.info("calling fuzzerTearDown function"); 410 try { 411 fuzzerTearDown.invoke(null); 412 } catch (InvocationTargetException e) { 413 Log.finding(e.getCause()); 414 System.exit(JAZZER_FINDING_EXIT_CODE); 415 } catch (Throwable t) { 416 Log.error(t); 417 System.exit(1); 418 } 419 } 420 dumpReproducer(byte[] data)421 private static void dumpReproducer(byte[] data) { 422 if (data == null) { 423 assert useFuzzedDataProvider; 424 fuzzedDataProvider.reset(); 425 data = fuzzedDataProvider.consumeRemainingAsBytes(); 426 } 427 MessageDigest digest; 428 try { 429 digest = MessageDigest.getInstance("SHA-1"); 430 } catch (NoSuchAlgorithmException e) { 431 throw new IllegalStateException("SHA-1 not available", e); 432 } 433 String dataSha1 = toHexString(digest.digest(data)); 434 435 if (!Opt.autofuzz.isEmpty()) { 436 fuzzedDataProvider.reset(); 437 FuzzTarget.dumpReproducer(fuzzedDataProvider, Opt.reproducerPath, dataSha1); 438 return; 439 } 440 441 String base64Data; 442 if (useFuzzedDataProvider) { 443 fuzzedDataProvider.reset(); 444 FuzzedDataProvider recordingFuzzedDataProvider = 445 RecordingFuzzedDataProvider.makeFuzzedDataProviderProxy(fuzzedDataProvider); 446 try { 447 fuzzTargetMethod.invokeExact(recordingFuzzedDataProvider); 448 if (JazzerInternal.lastFinding == null) { 449 Log.warn("Failed to reproduce crash when rerunning with recorder"); 450 } 451 } catch (Throwable ignored) { 452 // Expected. 453 } 454 try { 455 base64Data = RecordingFuzzedDataProvider.serializeFuzzedDataProviderProxy( 456 recordingFuzzedDataProvider); 457 } catch (IOException e) { 458 Log.error("Failed to create reproducer", e); 459 // Don't let libFuzzer print a native stack trace. 460 System.exit(1); 461 throw new IllegalStateException("Not reached"); 462 } 463 } else { 464 base64Data = Base64.getEncoder().encodeToString(data); 465 } 466 467 reproducerTemplate.dumpReproducer(base64Data, dataSha1); 468 } 469 470 /** 471 * Convert a byte array to a lower-case hex string. 472 * 473 * <p>The returned hex string always has {@code 2 * bytes.length} characters. 474 * 475 * @param bytes the bytes to convert 476 * @return a lower-case hex string representing the bytes 477 */ toHexString(byte[] bytes)478 private static String toHexString(byte[] bytes) { 479 String unpadded = new BigInteger(1, bytes).toString(16); 480 int numLeadingZeroes = 2 * bytes.length - unpadded.length(); 481 return String.join("", Collections.nCopies(numLeadingZeroes, "0")) + unpadded; 482 } 483 484 // Accessed by fuzz_target_runner.cpp. 485 @SuppressWarnings("unused") dumpAllStackTraces()486 private static void dumpAllStackTraces() { 487 ExceptionUtils.dumpAllStackTraces(); 488 } 489 copyToArray(long ptr, int length)490 private static byte[] copyToArray(long ptr, int length) { 491 // TODO: Use Unsafe.allocateUninitializedArray instead once Java 9 is the base. 492 byte[] array = new byte[length]; 493 UNSAFE.copyMemory(null, ptr, array, BYTE_ARRAY_OFFSET, length); 494 return array; 495 } 496 497 /** 498 * Starts libFuzzer via LLVMFuzzerRunDriver. 499 * 500 * @param args command-line arguments encoded in UTF-8 (not null-terminated) 501 * @return the return value of LLVMFuzzerRunDriver 502 */ startLibFuzzer(byte[][] args)503 private static int startLibFuzzer(byte[][] args) { 504 return FuzzTargetRunnerNatives.startLibFuzzer( 505 args, FuzzTargetRunner.class, Opt.experimentalMutator); 506 } 507 508 /** 509 * Causes libFuzzer to write the current input to disk as a crashing input and emit some 510 * information about it to stderr. 511 */ printCrashingInput()512 public static void printCrashingInput() { 513 FuzzTargetRunnerNatives.printCrashingInput(); 514 } 515 516 /** 517 * Returns the debug string of the current mutator. 518 * If no mutator is used, returns null. 519 */ mutatorDebugString()520 public static String mutatorDebugString() { 521 return mutator != null ? mutator.toString() : null; 522 } 523 524 /** 525 * Returns whether the current mutator has detected invalid corpus files. 526 * If no mutator is used, returns false. 527 */ invalidCorpusFilesPresent()528 public static boolean invalidCorpusFilesPresent() { 529 return mutator != null && invalidCorpusFileWarningShown; 530 } 531 532 /** 533 * Disables libFuzzer's fuzz target exit detection until the next call to {@link #runOne}. 534 * 535 * <p>Calling {@link System#exit} after having called this method will not trigger libFuzzer's 536 * exit hook that would otherwise print the "fuzz target exited" error message. This method should 537 * thus only be called after control has returned from the user-provided fuzz target. 538 */ temporarilyDisableLibfuzzerExitHook()539 private static void temporarilyDisableLibfuzzerExitHook() { 540 FuzzTargetRunnerNatives.temporarilyDisableLibfuzzerExitHook(); 541 } 542 } 543