| // 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 |