1 // Copyright 2021 Code Intelligence GmbH
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 #include "fuzz_target_runner.h"
16
17 #include <jni.h>
18
19 #include <fstream>
20 #include <iomanip>
21 #include <iostream>
22 #include <string>
23 #include <vector>
24
25 #include "absl/strings/escaping.h"
26 #include "absl/strings/str_cat.h"
27 #include "absl/strings/str_format.h"
28 #include "absl/strings/str_replace.h"
29 #include "absl/strings/str_split.h"
30 #include "absl/strings/substitute.h"
31 #include "coverage_tracker.h"
32 #include "fuzzed_data_provider.h"
33 #include "gflags/gflags.h"
34 #include "glog/logging.h"
35 #include "java_reproducer.h"
36 #include "java_reproducer_templates.h"
37 #include "utils.h"
38
39 DEFINE_string(
40 target_class, "",
41 "The Java class that contains the static fuzzerTestOneInput function");
42 DEFINE_string(target_args, "",
43 "Arguments passed to fuzzerInitialize as a String array. "
44 "Separated by space.");
45
46 DEFINE_uint32(keep_going, 0,
47 "Continue fuzzing until N distinct exception stack traces have"
48 "been encountered. Defaults to exit after the first finding "
49 "unless --autofuzz is specified.");
50 DEFINE_bool(dedup, true,
51 "Emit a dedup token for every finding. Defaults to true and is "
52 "required for --keep_going and --ignore.");
53 DEFINE_string(
54 ignore, "",
55 "Comma-separated list of crash dedup tokens to ignore. This is useful to "
56 "continue fuzzing before a crash is fixed.");
57
58 DEFINE_string(reproducer_path, ".",
59 "Path at which fuzzing reproducers are stored. Defaults to the "
60 "current directory.");
61 DEFINE_string(coverage_report, "",
62 "Path at which a coverage report is stored when the fuzzer "
63 "exits. If left empty, no report is generated (default)");
64
65 DEFINE_string(autofuzz, "",
66 "Fully qualified reference to a method on the classpath that "
67 "should be fuzzed automatically (example: System.out::println). "
68 "Fuzzing will continue even after a finding; specify "
69 "--keep_going=N to stop after N findings.");
70 DEFINE_string(autofuzz_ignore, "",
71 "Fully qualified class names of exceptions to ignore during "
72 "autofuzz. Separated by comma.");
73
74 DECLARE_bool(hooks);
75
76 constexpr auto kManifestUtilsClass =
77 "com/code_intelligence/jazzer/runtime/ManifestUtils";
78 constexpr auto kJazzerClass =
79 "com/code_intelligence/jazzer/runtime/JazzerInternal";
80 constexpr auto kAutofuzzFuzzTargetClass =
81 "com/code_intelligence/jazzer/autofuzz/FuzzTarget";
82
83 namespace jazzer {
84 // split a string on unescaped spaces
splitOnSpace(const std::string & s)85 std::vector<std::string> splitOnSpace(const std::string &s) {
86 if (s.empty()) {
87 return {};
88 }
89
90 std::vector<std::string> tokens;
91 std::size_t token_begin = 0;
92 for (std::size_t i = 1; i < s.size() - 1; i++) {
93 // only split if the space is not escaped by a backslash "\"
94 if (s[i] == ' ' && s[i - 1] != '\\') {
95 // don't split on multiple spaces
96 if (i > token_begin + 1)
97 tokens.push_back(s.substr(token_begin, i - token_begin));
98 token_begin = i + 1;
99 }
100 }
101 tokens.push_back(s.substr(token_begin));
102 return tokens;
103 }
104
FuzzTargetRunner(JVM & jvm,const std::vector<std::string> & additional_target_args)105 FuzzTargetRunner::FuzzTargetRunner(
106 JVM &jvm, const std::vector<std::string> &additional_target_args)
107 : ExceptionPrinter(jvm), jvm_(jvm), ignore_tokens_() {
108 auto &env = jvm.GetEnv();
109 if (!FLAGS_target_class.empty() && !FLAGS_autofuzz.empty()) {
110 std::cerr << "--target_class and --autofuzz cannot be specified together"
111 << std::endl;
112 exit(1);
113 }
114 if (!FLAGS_target_args.empty() && !FLAGS_autofuzz.empty()) {
115 std::cerr << "--target_args and --autofuzz cannot be specified together"
116 << std::endl;
117 exit(1);
118 }
119 if (FLAGS_autofuzz.empty() && !FLAGS_autofuzz_ignore.empty()) {
120 std::cerr << "--autofuzz_ignore requires --autofuzz" << std::endl;
121 exit(1);
122 }
123 if (FLAGS_target_class.empty() && FLAGS_autofuzz.empty()) {
124 FLAGS_target_class = DetectFuzzTargetClass();
125 }
126 // If automatically detecting the fuzz target class failed, we expect it as
127 // the value of the --target_class argument.
128 if (FLAGS_target_class.empty() && FLAGS_autofuzz.empty()) {
129 std::cerr << "Missing argument --target_class=<fuzz_target_class>"
130 << std::endl;
131 exit(1);
132 }
133 if (!FLAGS_autofuzz.empty()) {
134 FLAGS_target_class = kAutofuzzFuzzTargetClass;
135 if (FLAGS_keep_going == 0) {
136 FLAGS_keep_going = std::numeric_limits<gflags::uint32>::max();
137 }
138 // Pass the method reference string as the first argument to the generic
139 // autofuzz fuzz target. Subseqeuent arguments are interpreted as exception
140 // class names that should be ignored.
141 FLAGS_target_args = FLAGS_autofuzz;
142 if (!FLAGS_autofuzz_ignore.empty()) {
143 FLAGS_target_args = absl::StrCat(
144 FLAGS_target_args, " ",
145 absl::StrReplaceAll(FLAGS_autofuzz_ignore, {{",", " "}}));
146 }
147 }
148 // Set --keep_going to its real default.
149 if (FLAGS_keep_going == 0) {
150 FLAGS_keep_going = 1;
151 }
152 if ((!FLAGS_ignore.empty() || FLAGS_keep_going > 1) && !FLAGS_dedup) {
153 std::cerr << "--nodedup is not supported with --ignore or --keep_going"
154 << std::endl;
155 exit(1);
156 }
157 jazzer_ = jvm.FindClass(kJazzerClass);
158 last_finding_ =
159 env.GetStaticFieldID(jazzer_, "lastFinding", "Ljava/lang/Throwable;");
160
161 jclass_ = jvm.FindClass(FLAGS_target_class);
162 // one of the following functions is required:
163 // public static void fuzzerTestOneInput(byte[] input)
164 // public static void fuzzerTestOneInput(FuzzedDataProvider data)
165 fuzzer_test_one_input_bytes_ =
166 jvm.GetStaticMethodID(jclass_, "fuzzerTestOneInput", "([B)V", false);
167 fuzzer_test_one_input_data_ = jvm.GetStaticMethodID(
168 jclass_, "fuzzerTestOneInput",
169 "(Lcom/code_intelligence/jazzer/api/FuzzedDataProvider;)V", false);
170 bool using_bytes = fuzzer_test_one_input_bytes_ != nullptr;
171 bool using_data = fuzzer_test_one_input_data_ != nullptr;
172 // Fail if none ore both of the two possible fuzzerTestOneInput versions is
173 // defined in the class.
174 if (using_bytes == using_data) {
175 LOG(ERROR) << FLAGS_target_class
176 << " must define exactly one of the following two functions:";
177 LOG(ERROR) << "public static void fuzzerTestOneInput(byte[] ...)";
178 LOG(ERROR)
179 << "public static void fuzzerTestOneInput(FuzzedDataProvider ...)";
180 LOG(ERROR) << "Note: Fuzz targets returning boolean are no longer "
181 "supported; exceptions should be thrown instead of "
182 "returning true.";
183 exit(1);
184 }
185
186 // check existence of optional methods for initialization and destruction
187 fuzzer_initialize_ =
188 jvm.GetStaticMethodID(jclass_, "fuzzerInitialize", "()V", false);
189 fuzzer_tear_down_ =
190 jvm.GetStaticMethodID(jclass_, "fuzzerTearDown", "()V", false);
191 fuzzer_initialize_with_args_ = jvm.GetStaticMethodID(
192 jclass_, "fuzzerInitialize", "([Ljava/lang/String;)V", false);
193
194 auto fuzz_target_args_tokens = splitOnSpace(FLAGS_target_args);
195 fuzz_target_args_tokens.insert(fuzz_target_args_tokens.end(),
196 additional_target_args.begin(),
197 additional_target_args.end());
198
199 if (fuzzer_initialize_with_args_) {
200 // fuzzerInitialize with arguments gets priority
201 jclass string_class = jvm.FindClass("java/lang/String");
202 jobjectArray arg_array = jvm.GetEnv().NewObjectArray(
203 fuzz_target_args_tokens.size(), string_class, nullptr);
204 for (jint i = 0; i < fuzz_target_args_tokens.size(); i++) {
205 jstring str = env.NewStringUTF(fuzz_target_args_tokens[i].c_str());
206 env.SetObjectArrayElement(arg_array, i, str);
207 }
208 env.CallStaticObjectMethod(jclass_, fuzzer_initialize_with_args_,
209 arg_array);
210 } else if (fuzzer_initialize_) {
211 env.CallStaticVoidMethod(jclass_, fuzzer_initialize_);
212 } else {
213 LOG(INFO) << "did not call any fuzz target initialize functions";
214 }
215
216 if (jthrowable exception = env.ExceptionOccurred()) {
217 LOG(ERROR) << "== Java Exception in fuzzerInitialize: ";
218 LOG(ERROR) << getStackTrace(exception);
219 std::exit(1);
220 }
221
222 if (FLAGS_hooks) {
223 CoverageTracker::RecordInitialCoverage(env);
224 }
225 SetUpFuzzedDataProvider(jvm_.GetEnv());
226
227 // Parse a comma-separated list of hex dedup tokens.
228 std::vector<std::string> str_ignore_tokens =
229 absl::StrSplit(FLAGS_ignore, ',');
230 for (const std::string &str_token : str_ignore_tokens) {
231 if (str_token.empty()) continue;
232 try {
233 ignore_tokens_.push_back(std::stoull(str_token, nullptr, 16));
234 } catch (...) {
235 LOG(ERROR) << "Invalid dedup token (expected up to 16 hex digits): '"
236 << str_token << "'";
237 // Don't let libFuzzer print a crash stack trace.
238 _Exit(1);
239 }
240 }
241 }
242
~FuzzTargetRunner()243 FuzzTargetRunner::~FuzzTargetRunner() {
244 if (FLAGS_hooks && !FLAGS_coverage_report.empty()) {
245 std::string report = CoverageTracker::ComputeCoverage(jvm_.GetEnv());
246 std::ofstream report_file(FLAGS_coverage_report);
247 if (report_file) {
248 report_file << report << std::flush;
249 } else {
250 LOG(ERROR) << "Failed to write coverage report to "
251 << FLAGS_coverage_report;
252 }
253 }
254 if (fuzzer_tear_down_ != nullptr) {
255 std::cerr << "calling fuzzer teardown function" << std::endl;
256 jvm_.GetEnv().CallStaticVoidMethod(jclass_, fuzzer_tear_down_);
257 if (jthrowable exception = jvm_.GetEnv().ExceptionOccurred())
258 std::cerr << getStackTrace(exception) << std::endl;
259 }
260 }
261
Run(const uint8_t * data,const std::size_t size)262 RunResult FuzzTargetRunner::Run(const uint8_t *data, const std::size_t size) {
263 auto &env = jvm_.GetEnv();
264 static std::size_t run_count = 0;
265 if (run_count < 2) {
266 run_count++;
267 // For the first two runs only, replay the coverage recorded from static
268 // initializers. libFuzzer cleared the coverage map after they ran and could
269 // fail to see any coverage, triggering an early exit, if we don't replay it
270 // here.
271 // https://github.com/llvm/llvm-project/blob/957a5e987444d3193575d6ad8afe6c75da00d794/compiler-rt/lib/fuzzer/FuzzerLoop.cpp#L804-L809
272 CoverageTracker::ReplayInitialCoverage(env);
273 }
274 if (fuzzer_test_one_input_data_ != nullptr) {
275 FeedFuzzedDataProvider(data, size);
276 env.CallStaticVoidMethod(jclass_, fuzzer_test_one_input_data_,
277 GetFuzzedDataProviderJavaObject(jvm_));
278 } else {
279 jbyteArray byte_array = env.NewByteArray(size);
280 if (byte_array == nullptr) {
281 env.ExceptionDescribe();
282 throw std::runtime_error(std::string("Cannot create byte array"));
283 }
284 env.SetByteArrayRegion(byte_array, 0, size,
285 reinterpret_cast<const jbyte *>(data));
286 env.CallStaticVoidMethod(jclass_, fuzzer_test_one_input_bytes_, byte_array);
287 env.DeleteLocalRef(byte_array);
288 }
289
290 const auto finding = GetFinding();
291 if (finding != nullptr) {
292 jlong dedup_token = computeDedupToken(finding);
293 // Check whether this stack trace has been encountered before if
294 // `--keep_going` has been supplied.
295 if (dedup_token != 0 && FLAGS_keep_going > 1 &&
296 std::find(ignore_tokens_.cbegin(), ignore_tokens_.cend(),
297 dedup_token) != ignore_tokens_.end()) {
298 env.DeleteLocalRef(finding);
299 return RunResult::kOk;
300 } else {
301 ignore_tokens_.push_back(dedup_token);
302 std::cout << std::endl;
303 std::cerr << "== Java Exception: " << getStackTrace(finding);
304 env.DeleteLocalRef(finding);
305 if (FLAGS_dedup) {
306 std::cout << "DEDUP_TOKEN: " << std::hex << std::setfill('0')
307 << std::setw(16) << dedup_token << std::endl;
308 }
309 if (ignore_tokens_.size() < static_cast<std::size_t>(FLAGS_keep_going)) {
310 return RunResult::kDumpAndContinue;
311 } else {
312 return RunResult::kException;
313 }
314 }
315 }
316 return RunResult::kOk;
317 }
318
319 // Returns a fuzzer finding as a Throwable (or nullptr if there is none),
320 // clearing any JVM exceptions in the process.
GetFinding() const321 jthrowable FuzzTargetRunner::GetFinding() const {
322 auto &env = jvm_.GetEnv();
323 jthrowable unprocessed_finding = nullptr;
324 if (env.ExceptionCheck()) {
325 unprocessed_finding = env.ExceptionOccurred();
326 env.ExceptionClear();
327 }
328 // Explicitly reported findings take precedence over uncaught exceptions.
329 if (auto reported_finding =
330 (jthrowable)env.GetStaticObjectField(jazzer_, last_finding_);
331 reported_finding != nullptr) {
332 env.DeleteLocalRef(unprocessed_finding);
333 unprocessed_finding = reported_finding;
334 }
335 jthrowable processed_finding = preprocessException(unprocessed_finding);
336 env.DeleteLocalRef(unprocessed_finding);
337 return processed_finding;
338 }
339
DumpReproducer(const uint8_t * data,std::size_t size)340 void FuzzTargetRunner::DumpReproducer(const uint8_t *data, std::size_t size) {
341 auto &env = jvm_.GetEnv();
342 std::string base64_data;
343 if (fuzzer_test_one_input_data_) {
344 // Record the data retrieved from the FuzzedDataProvider and supply it to a
345 // Java-only CannedFuzzedDataProvider in the reproducer.
346 FeedFuzzedDataProvider(data, size);
347 jobject recorder = GetRecordingFuzzedDataProviderJavaObject(jvm_);
348 env.CallStaticVoidMethod(jclass_, fuzzer_test_one_input_data_, recorder);
349 const auto finding = GetFinding();
350 if (finding == nullptr) {
351 LOG(ERROR) << "Failed to reproduce crash when rerunning with recorder";
352 return;
353 }
354 base64_data = SerializeRecordingFuzzedDataProvider(jvm_, recorder);
355 } else {
356 absl::string_view data_str(reinterpret_cast<const char *>(data), size);
357 absl::Base64Escape(data_str, &base64_data);
358 }
359 const char *fuzz_target_call = fuzzer_test_one_input_data_
360 ? kTestOneInputWithData
361 : kTestOneInputWithBytes;
362 std::string data_sha1 = jazzer::Sha1Hash(data, size);
363 std::string reproducer =
364 absl::Substitute(kBaseReproducer, data_sha1, base64_data,
365 FLAGS_target_class, fuzz_target_call);
366 std::string reproducer_filename = absl::StrFormat("Crash_%s.java", data_sha1);
367 std::string reproducer_full_path = absl::StrFormat(
368 "%s%c%s", FLAGS_reproducer_path, kPathSeparator, reproducer_filename);
369 std::ofstream reproducer_out(reproducer_full_path);
370 reproducer_out << reproducer;
371 std::cout << absl::StrFormat(
372 "reproducer_path='%s'; Java reproducer written to %s",
373 FLAGS_reproducer_path, reproducer_full_path)
374 << std::endl;
375 }
376
DetectFuzzTargetClass() const377 std::string FuzzTargetRunner::DetectFuzzTargetClass() const {
378 jclass manifest_utils = jvm_.FindClass(kManifestUtilsClass);
379 jmethodID detect_fuzz_target_class = jvm_.GetStaticMethodID(
380 manifest_utils, "detectFuzzTargetClass", "()Ljava/lang/String;", true);
381 auto &env = jvm_.GetEnv();
382 auto jni_fuzz_target_class = (jstring)(env.CallStaticObjectMethod(
383 manifest_utils, detect_fuzz_target_class));
384 if (env.ExceptionCheck()) {
385 env.ExceptionDescribe();
386 exit(1);
387 }
388 if (jni_fuzz_target_class == nullptr) return "";
389
390 const char *fuzz_target_class_cstr =
391 env.GetStringUTFChars(jni_fuzz_target_class, nullptr);
392 std::string fuzz_target_class = std::string(fuzz_target_class_cstr);
393 env.ReleaseStringUTFChars(jni_fuzz_target_class, fuzz_target_class_cstr);
394 env.DeleteLocalRef(jni_fuzz_target_class);
395
396 return fuzz_target_class;
397 }
398 } // namespace jazzer
399