399 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C++
		
	
	
	
			
		
		
	
	
			399 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C++
		
	
	
	
| // Copyright 2021 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.
 | |
| 
 | |
| #include "fuzz_target_runner.h"
 | |
| 
 | |
| #include <jni.h>
 | |
| 
 | |
| #include <fstream>
 | |
| #include <iomanip>
 | |
| #include <iostream>
 | |
| #include <string>
 | |
| #include <vector>
 | |
| 
 | |
| #include "absl/strings/escaping.h"
 | |
| #include "absl/strings/str_cat.h"
 | |
| #include "absl/strings/str_format.h"
 | |
| #include "absl/strings/str_replace.h"
 | |
| #include "absl/strings/str_split.h"
 | |
| #include "absl/strings/substitute.h"
 | |
| #include "coverage_tracker.h"
 | |
| #include "fuzzed_data_provider.h"
 | |
| #include "gflags/gflags.h"
 | |
| #include "glog/logging.h"
 | |
| #include "java_reproducer.h"
 | |
| #include "java_reproducer_templates.h"
 | |
| #include "utils.h"
 | |
| 
 | |
| DEFINE_string(
 | |
|     target_class, "",
 | |
|     "The Java class that contains the static fuzzerTestOneInput function");
 | |
| DEFINE_string(target_args, "",
 | |
|               "Arguments passed to fuzzerInitialize as a String array. "
 | |
|               "Separated by space.");
 | |
| 
 | |
| DEFINE_uint32(keep_going, 0,
 | |
|               "Continue fuzzing until N distinct exception stack traces have"
 | |
|               "been encountered. Defaults to exit after the first finding "
 | |
|               "unless --autofuzz is specified.");
 | |
| DEFINE_bool(dedup, true,
 | |
|             "Emit a dedup token for every finding. Defaults to true and is "
 | |
|             "required for --keep_going and --ignore.");
 | |
| DEFINE_string(
 | |
|     ignore, "",
 | |
|     "Comma-separated list of crash dedup tokens to ignore. This is useful to "
 | |
|     "continue fuzzing before a crash is fixed.");
 | |
| 
 | |
| DEFINE_string(reproducer_path, ".",
 | |
|               "Path at which fuzzing reproducers are stored. Defaults to the "
 | |
|               "current directory.");
 | |
| DEFINE_string(coverage_report, "",
 | |
|               "Path at which a coverage report is stored when the fuzzer "
 | |
|               "exits. If left empty, no report is generated (default)");
 | |
| 
 | |
| DEFINE_string(autofuzz, "",
 | |
|               "Fully qualified reference to a method on the classpath that "
 | |
|               "should be fuzzed automatically (example: System.out::println). "
 | |
|               "Fuzzing will continue even after a finding; specify "
 | |
|               "--keep_going=N to stop after N findings.");
 | |
| DEFINE_string(autofuzz_ignore, "",
 | |
|               "Fully qualified class names of exceptions to ignore during "
 | |
|               "autofuzz. Separated by comma.");
 | |
| 
 | |
| DECLARE_bool(hooks);
 | |
| 
 | |
| constexpr auto kManifestUtilsClass =
 | |
|     "com/code_intelligence/jazzer/runtime/ManifestUtils";
 | |
| constexpr auto kJazzerClass =
 | |
|     "com/code_intelligence/jazzer/runtime/JazzerInternal";
 | |
| constexpr auto kAutofuzzFuzzTargetClass =
 | |
|     "com/code_intelligence/jazzer/autofuzz/FuzzTarget";
 | |
| 
 | |
| namespace jazzer {
 | |
| // split a string on unescaped spaces
 | |
| std::vector<std::string> splitOnSpace(const std::string &s) {
 | |
|   if (s.empty()) {
 | |
|     return {};
 | |
|   }
 | |
| 
 | |
|   std::vector<std::string> tokens;
 | |
|   std::size_t token_begin = 0;
 | |
|   for (std::size_t i = 1; i < s.size() - 1; i++) {
 | |
|     // only split if the space is not escaped by a backslash "\"
 | |
|     if (s[i] == ' ' && s[i - 1] != '\\') {
 | |
|       // don't split on multiple spaces
 | |
|       if (i > token_begin + 1)
 | |
|         tokens.push_back(s.substr(token_begin, i - token_begin));
 | |
|       token_begin = i + 1;
 | |
|     }
 | |
|   }
 | |
|   tokens.push_back(s.substr(token_begin));
 | |
|   return tokens;
 | |
| }
 | |
| 
 | |
| FuzzTargetRunner::FuzzTargetRunner(
 | |
|     JVM &jvm, const std::vector<std::string> &additional_target_args)
 | |
|     : ExceptionPrinter(jvm), jvm_(jvm), ignore_tokens_() {
 | |
|   auto &env = jvm.GetEnv();
 | |
|   if (!FLAGS_target_class.empty() && !FLAGS_autofuzz.empty()) {
 | |
|     std::cerr << "--target_class and --autofuzz cannot be specified together"
 | |
|               << std::endl;
 | |
|     exit(1);
 | |
|   }
 | |
|   if (!FLAGS_target_args.empty() && !FLAGS_autofuzz.empty()) {
 | |
|     std::cerr << "--target_args and --autofuzz cannot be specified together"
 | |
|               << std::endl;
 | |
|     exit(1);
 | |
|   }
 | |
|   if (FLAGS_autofuzz.empty() && !FLAGS_autofuzz_ignore.empty()) {
 | |
|     std::cerr << "--autofuzz_ignore requires --autofuzz" << std::endl;
 | |
|     exit(1);
 | |
|   }
 | |
|   if (FLAGS_target_class.empty() && FLAGS_autofuzz.empty()) {
 | |
|     FLAGS_target_class = DetectFuzzTargetClass();
 | |
|   }
 | |
|   // If automatically detecting the fuzz target class failed, we expect it as
 | |
|   // the value of the --target_class argument.
 | |
|   if (FLAGS_target_class.empty() && FLAGS_autofuzz.empty()) {
 | |
|     std::cerr << "Missing argument --target_class=<fuzz_target_class>"
 | |
|               << std::endl;
 | |
|     exit(1);
 | |
|   }
 | |
|   if (!FLAGS_autofuzz.empty()) {
 | |
|     FLAGS_target_class = kAutofuzzFuzzTargetClass;
 | |
|     if (FLAGS_keep_going == 0) {
 | |
|       FLAGS_keep_going = std::numeric_limits<gflags::uint32>::max();
 | |
|     }
 | |
|     // Pass the method reference string as the first argument to the generic
 | |
|     // autofuzz fuzz target. Subseqeuent arguments are interpreted as exception
 | |
|     // class names that should be ignored.
 | |
|     FLAGS_target_args = FLAGS_autofuzz;
 | |
|     if (!FLAGS_autofuzz_ignore.empty()) {
 | |
|       FLAGS_target_args = absl::StrCat(
 | |
|           FLAGS_target_args, " ",
 | |
|           absl::StrReplaceAll(FLAGS_autofuzz_ignore, {{",", " "}}));
 | |
|     }
 | |
|   }
 | |
|   // Set --keep_going to its real default.
 | |
|   if (FLAGS_keep_going == 0) {
 | |
|     FLAGS_keep_going = 1;
 | |
|   }
 | |
|   if ((!FLAGS_ignore.empty() || FLAGS_keep_going > 1) && !FLAGS_dedup) {
 | |
|     std::cerr << "--nodedup is not supported with --ignore or --keep_going"
 | |
|               << std::endl;
 | |
|     exit(1);
 | |
|   }
 | |
|   jazzer_ = jvm.FindClass(kJazzerClass);
 | |
|   last_finding_ =
 | |
|       env.GetStaticFieldID(jazzer_, "lastFinding", "Ljava/lang/Throwable;");
 | |
| 
 | |
|   jclass_ = jvm.FindClass(FLAGS_target_class);
 | |
|   // one of the following functions is required:
 | |
|   //    public static void fuzzerTestOneInput(byte[] input)
 | |
|   //    public static void fuzzerTestOneInput(FuzzedDataProvider data)
 | |
|   fuzzer_test_one_input_bytes_ =
 | |
|       jvm.GetStaticMethodID(jclass_, "fuzzerTestOneInput", "([B)V", false);
 | |
|   fuzzer_test_one_input_data_ = jvm.GetStaticMethodID(
 | |
|       jclass_, "fuzzerTestOneInput",
 | |
|       "(Lcom/code_intelligence/jazzer/api/FuzzedDataProvider;)V", false);
 | |
|   bool using_bytes = fuzzer_test_one_input_bytes_ != nullptr;
 | |
|   bool using_data = fuzzer_test_one_input_data_ != nullptr;
 | |
|   // Fail if none ore both of the two possible fuzzerTestOneInput versions is
 | |
|   // defined in the class.
 | |
|   if (using_bytes == using_data) {
 | |
|     LOG(ERROR) << FLAGS_target_class
 | |
|                << " must define exactly one of the following two functions:";
 | |
|     LOG(ERROR) << "public static void fuzzerTestOneInput(byte[] ...)";
 | |
|     LOG(ERROR)
 | |
|         << "public static void fuzzerTestOneInput(FuzzedDataProvider ...)";
 | |
|     LOG(ERROR) << "Note: Fuzz targets returning boolean are no longer "
 | |
|                   "supported; exceptions should be thrown instead of "
 | |
|                   "returning true.";
 | |
|     exit(1);
 | |
|   }
 | |
| 
 | |
|   // check existence of optional methods for initialization and destruction
 | |
|   fuzzer_initialize_ =
 | |
|       jvm.GetStaticMethodID(jclass_, "fuzzerInitialize", "()V", false);
 | |
|   fuzzer_tear_down_ =
 | |
|       jvm.GetStaticMethodID(jclass_, "fuzzerTearDown", "()V", false);
 | |
|   fuzzer_initialize_with_args_ = jvm.GetStaticMethodID(
 | |
|       jclass_, "fuzzerInitialize", "([Ljava/lang/String;)V", false);
 | |
| 
 | |
|   auto fuzz_target_args_tokens = splitOnSpace(FLAGS_target_args);
 | |
|   fuzz_target_args_tokens.insert(fuzz_target_args_tokens.end(),
 | |
|                                  additional_target_args.begin(),
 | |
|                                  additional_target_args.end());
 | |
| 
 | |
|   if (fuzzer_initialize_with_args_) {
 | |
|     // fuzzerInitialize with arguments gets priority
 | |
|     jclass string_class = jvm.FindClass("java/lang/String");
 | |
|     jobjectArray arg_array = jvm.GetEnv().NewObjectArray(
 | |
|         fuzz_target_args_tokens.size(), string_class, nullptr);
 | |
|     for (jint i = 0; i < fuzz_target_args_tokens.size(); i++) {
 | |
|       jstring str = env.NewStringUTF(fuzz_target_args_tokens[i].c_str());
 | |
|       env.SetObjectArrayElement(arg_array, i, str);
 | |
|     }
 | |
|     env.CallStaticObjectMethod(jclass_, fuzzer_initialize_with_args_,
 | |
|                                arg_array);
 | |
|   } else if (fuzzer_initialize_) {
 | |
|     env.CallStaticVoidMethod(jclass_, fuzzer_initialize_);
 | |
|   } else {
 | |
|     LOG(INFO) << "did not call any fuzz target initialize functions";
 | |
|   }
 | |
| 
 | |
|   if (jthrowable exception = env.ExceptionOccurred()) {
 | |
|     LOG(ERROR) << "== Java Exception in fuzzerInitialize: ";
 | |
|     LOG(ERROR) << getStackTrace(exception);
 | |
|     std::exit(1);
 | |
|   }
 | |
| 
 | |
|   if (FLAGS_hooks) {
 | |
|     CoverageTracker::RecordInitialCoverage(env);
 | |
|   }
 | |
|   SetUpFuzzedDataProvider(jvm_.GetEnv());
 | |
| 
 | |
|   // Parse a comma-separated list of hex dedup tokens.
 | |
|   std::vector<std::string> str_ignore_tokens =
 | |
|       absl::StrSplit(FLAGS_ignore, ',');
 | |
|   for (const std::string &str_token : str_ignore_tokens) {
 | |
|     if (str_token.empty()) continue;
 | |
|     try {
 | |
|       ignore_tokens_.push_back(std::stoull(str_token, nullptr, 16));
 | |
|     } catch (...) {
 | |
|       LOG(ERROR) << "Invalid dedup token (expected up to 16 hex digits): '"
 | |
|                  << str_token << "'";
 | |
|       // Don't let libFuzzer print a crash stack trace.
 | |
|       _Exit(1);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| FuzzTargetRunner::~FuzzTargetRunner() {
 | |
|   if (FLAGS_hooks && !FLAGS_coverage_report.empty()) {
 | |
|     std::string report = CoverageTracker::ComputeCoverage(jvm_.GetEnv());
 | |
|     std::ofstream report_file(FLAGS_coverage_report);
 | |
|     if (report_file) {
 | |
|       report_file << report << std::flush;
 | |
|     } else {
 | |
|       LOG(ERROR) << "Failed to write coverage report to "
 | |
|                  << FLAGS_coverage_report;
 | |
|     }
 | |
|   }
 | |
|   if (fuzzer_tear_down_ != nullptr) {
 | |
|     std::cerr << "calling fuzzer teardown function" << std::endl;
 | |
|     jvm_.GetEnv().CallStaticVoidMethod(jclass_, fuzzer_tear_down_);
 | |
|     if (jthrowable exception = jvm_.GetEnv().ExceptionOccurred())
 | |
|       std::cerr << getStackTrace(exception) << std::endl;
 | |
|   }
 | |
| }
 | |
| 
 | |
| RunResult FuzzTargetRunner::Run(const uint8_t *data, const std::size_t size) {
 | |
|   auto &env = jvm_.GetEnv();
 | |
|   static std::size_t run_count = 0;
 | |
|   if (run_count < 2) {
 | |
|     run_count++;
 | |
|     // For the first two runs only, replay the coverage recorded from static
 | |
|     // initializers. libFuzzer cleared the coverage map after they ran and could
 | |
|     // fail to see any coverage, triggering an early exit, if we don't replay it
 | |
|     // here.
 | |
|     // https://github.com/llvm/llvm-project/blob/957a5e987444d3193575d6ad8afe6c75da00d794/compiler-rt/lib/fuzzer/FuzzerLoop.cpp#L804-L809
 | |
|     CoverageTracker::ReplayInitialCoverage(env);
 | |
|   }
 | |
|   if (fuzzer_test_one_input_data_ != nullptr) {
 | |
|     FeedFuzzedDataProvider(data, size);
 | |
|     env.CallStaticVoidMethod(jclass_, fuzzer_test_one_input_data_,
 | |
|                              GetFuzzedDataProviderJavaObject(jvm_));
 | |
|   } else {
 | |
|     jbyteArray byte_array = env.NewByteArray(size);
 | |
|     if (byte_array == nullptr) {
 | |
|       env.ExceptionDescribe();
 | |
|       throw std::runtime_error(std::string("Cannot create byte array"));
 | |
|     }
 | |
|     env.SetByteArrayRegion(byte_array, 0, size,
 | |
|                            reinterpret_cast<const jbyte *>(data));
 | |
|     env.CallStaticVoidMethod(jclass_, fuzzer_test_one_input_bytes_, byte_array);
 | |
|     env.DeleteLocalRef(byte_array);
 | |
|   }
 | |
| 
 | |
|   const auto finding = GetFinding();
 | |
|   if (finding != nullptr) {
 | |
|     jlong dedup_token = computeDedupToken(finding);
 | |
|     // Check whether this stack trace has been encountered before if
 | |
|     // `--keep_going` has been supplied.
 | |
|     if (dedup_token != 0 && FLAGS_keep_going > 1 &&
 | |
|         std::find(ignore_tokens_.cbegin(), ignore_tokens_.cend(),
 | |
|                   dedup_token) != ignore_tokens_.end()) {
 | |
|       env.DeleteLocalRef(finding);
 | |
|       return RunResult::kOk;
 | |
|     } else {
 | |
|       ignore_tokens_.push_back(dedup_token);
 | |
|       std::cout << std::endl;
 | |
|       std::cerr << "== Java Exception: " << getStackTrace(finding);
 | |
|       env.DeleteLocalRef(finding);
 | |
|       if (FLAGS_dedup) {
 | |
|         std::cout << "DEDUP_TOKEN: " << std::hex << std::setfill('0')
 | |
|                   << std::setw(16) << dedup_token << std::endl;
 | |
|       }
 | |
|       if (ignore_tokens_.size() < static_cast<std::size_t>(FLAGS_keep_going)) {
 | |
|         return RunResult::kDumpAndContinue;
 | |
|       } else {
 | |
|         return RunResult::kException;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   return RunResult::kOk;
 | |
| }
 | |
| 
 | |
| // Returns a fuzzer finding as a Throwable (or nullptr if there is none),
 | |
| // clearing any JVM exceptions in the process.
 | |
| jthrowable FuzzTargetRunner::GetFinding() const {
 | |
|   auto &env = jvm_.GetEnv();
 | |
|   jthrowable unprocessed_finding = nullptr;
 | |
|   if (env.ExceptionCheck()) {
 | |
|     unprocessed_finding = env.ExceptionOccurred();
 | |
|     env.ExceptionClear();
 | |
|   }
 | |
|   // Explicitly reported findings take precedence over uncaught exceptions.
 | |
|   if (auto reported_finding =
 | |
|           (jthrowable)env.GetStaticObjectField(jazzer_, last_finding_);
 | |
|       reported_finding != nullptr) {
 | |
|     env.DeleteLocalRef(unprocessed_finding);
 | |
|     unprocessed_finding = reported_finding;
 | |
|   }
 | |
|   jthrowable processed_finding = preprocessException(unprocessed_finding);
 | |
|   env.DeleteLocalRef(unprocessed_finding);
 | |
|   return processed_finding;
 | |
| }
 | |
| 
 | |
| void FuzzTargetRunner::DumpReproducer(const uint8_t *data, std::size_t size) {
 | |
|   auto &env = jvm_.GetEnv();
 | |
|   std::string base64_data;
 | |
|   if (fuzzer_test_one_input_data_) {
 | |
|     // Record the data retrieved from the FuzzedDataProvider and supply it to a
 | |
|     // Java-only CannedFuzzedDataProvider in the reproducer.
 | |
|     FeedFuzzedDataProvider(data, size);
 | |
|     jobject recorder = GetRecordingFuzzedDataProviderJavaObject(jvm_);
 | |
|     env.CallStaticVoidMethod(jclass_, fuzzer_test_one_input_data_, recorder);
 | |
|     const auto finding = GetFinding();
 | |
|     if (finding == nullptr) {
 | |
|       LOG(ERROR) << "Failed to reproduce crash when rerunning with recorder";
 | |
|       return;
 | |
|     }
 | |
|     base64_data = SerializeRecordingFuzzedDataProvider(jvm_, recorder);
 | |
|   } else {
 | |
|     absl::string_view data_str(reinterpret_cast<const char *>(data), size);
 | |
|     absl::Base64Escape(data_str, &base64_data);
 | |
|   }
 | |
|   const char *fuzz_target_call = fuzzer_test_one_input_data_
 | |
|                                      ? kTestOneInputWithData
 | |
|                                      : kTestOneInputWithBytes;
 | |
|   std::string data_sha1 = jazzer::Sha1Hash(data, size);
 | |
|   std::string reproducer =
 | |
|       absl::Substitute(kBaseReproducer, data_sha1, base64_data,
 | |
|                        FLAGS_target_class, fuzz_target_call);
 | |
|   std::string reproducer_filename = absl::StrFormat("Crash_%s.java", data_sha1);
 | |
|   std::string reproducer_full_path = absl::StrFormat(
 | |
|       "%s%c%s", FLAGS_reproducer_path, kPathSeparator, reproducer_filename);
 | |
|   std::ofstream reproducer_out(reproducer_full_path);
 | |
|   reproducer_out << reproducer;
 | |
|   std::cout << absl::StrFormat(
 | |
|                    "reproducer_path='%s'; Java reproducer written to %s",
 | |
|                    FLAGS_reproducer_path, reproducer_full_path)
 | |
|             << std::endl;
 | |
| }
 | |
| 
 | |
| std::string FuzzTargetRunner::DetectFuzzTargetClass() const {
 | |
|   jclass manifest_utils = jvm_.FindClass(kManifestUtilsClass);
 | |
|   jmethodID detect_fuzz_target_class = jvm_.GetStaticMethodID(
 | |
|       manifest_utils, "detectFuzzTargetClass", "()Ljava/lang/String;", true);
 | |
|   auto &env = jvm_.GetEnv();
 | |
|   auto jni_fuzz_target_class = (jstring)(env.CallStaticObjectMethod(
 | |
|       manifest_utils, detect_fuzz_target_class));
 | |
|   if (env.ExceptionCheck()) {
 | |
|     env.ExceptionDescribe();
 | |
|     exit(1);
 | |
|   }
 | |
|   if (jni_fuzz_target_class == nullptr) return "";
 | |
| 
 | |
|   const char *fuzz_target_class_cstr =
 | |
|       env.GetStringUTFChars(jni_fuzz_target_class, nullptr);
 | |
|   std::string fuzz_target_class = std::string(fuzz_target_class_cstr);
 | |
|   env.ReleaseStringUTFChars(jni_fuzz_target_class, fuzz_target_class_cstr);
 | |
|   env.DeleteLocalRef(jni_fuzz_target_class);
 | |
| 
 | |
|   return fuzz_target_class;
 | |
| }
 | |
| }  // namespace jazzer
 |