android13/external/jazzer-api/driver/fuzz_target_runner.cpp

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