diff --git a/centipede/BUILD b/centipede/BUILD index 6835c2ed..39282425 100644 --- a/centipede/BUILD +++ b/centipede/BUILD @@ -913,6 +913,7 @@ cc_library( ":environment", ":mutation_input", ":runner_result", + ":stop", "@com_google_absl//absl/log", "@com_google_absl//absl/log:check", "@com_google_fuzztest//common:defs", @@ -1121,6 +1122,7 @@ cc_library( ":thread_pool", ":util", ":workdir", + "@com_google_absl//absl/container:flat_hash_set", "@com_google_absl//absl/log", "@com_google_absl//absl/log:check", "@com_google_absl//absl/random", @@ -1688,8 +1690,11 @@ cc_test( ":feature", ":seed_corpus_maker_lib", ":workdir", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/strings", + "@com_google_fuzztest//common:defs", "@com_google_fuzztest//common:logging", + "@com_google_fuzztest//common:remote_file", "@com_google_fuzztest//common:test_util", "@com_google_googletest//:gtest_main", ], diff --git a/centipede/centipede_callbacks.cc b/centipede/centipede_callbacks.cc index 436e6d24..1b3cd7b8 100644 --- a/centipede/centipede_callbacks.cc +++ b/centipede/centipede_callbacks.cc @@ -231,7 +231,10 @@ int CentipedeCallbacks::ExecuteCentipedeSancovBinaryWithShmem( << env_.shmem_size_mb; } - if (env_.print_runner_log) PrintExecutionLog(); + if (env_.print_runner_log) { + LOG(INFO) << "Runner execution log:"; + PrintExecutionLog(); + } if (retval != EXIT_SUCCESS) { ReadFromLocalFile(execute_log_path_, batch_result.log()); @@ -264,6 +267,11 @@ bool CentipedeCallbacks::GetSeedsViaExternalBinary( .temp_file_path = temp_input_file_path_}}; const int retval = cmd.Execute(); + if (env_.print_runner_log) { + LOG(INFO) << "Getting seeds via external binary returns " << retval; + PrintExecutionLog(); + } + std::vector seed_input_filenames; for (const auto &dir_ent : std::filesystem::directory_iterator(output_dir)) { seed_input_filenames.push_back(dir_ent.path().filename()); @@ -340,6 +348,9 @@ bool CentipedeCallbacks::MutateViaExternalBinary( } if (env_.print_runner_log || retval != EXIT_SUCCESS) { PrintExecutionLog(); + } else if (env_.print_runner_log) { + LOG(INFO) << "Custom mutator log:"; + PrintExecutionLog(); } // Read all mutants. diff --git a/centipede/centipede_default_callbacks.cc b/centipede/centipede_default_callbacks.cc index 6d211215..80203d55 100644 --- a/centipede/centipede_default_callbacks.cc +++ b/centipede/centipede_default_callbacks.cc @@ -25,6 +25,7 @@ #include "./centipede/environment.h" #include "./centipede/mutation_input.h" #include "./centipede/runner_result.h" +#include "./centipede/stop.h" #include "./common/defs.h" #include "./common/logging.h" // IWYU pragma: keep @@ -83,6 +84,11 @@ void CentipedeDefaultCallbacks::Mutate( LOG_FIRST_N(WARNING, 5) << "Custom mutator returned no mutants: falling back to internal " "default mutator"; + } else if (ShouldStop()) { + LOG(WARNING) << "Custom mutator failed, but ignored since the stop " + "condition it met. Possibly what triggered the stop " + "condition also interrupted the mutator."; + return; } else { LOG(WARNING) << "Custom mutator undetected or misbehaving:"; CHECK(!custom_mutator_is_usable_.has_value()) diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc index dbd309cb..1a7c41b0 100644 --- a/centipede/centipede_interface.cc +++ b/centipede/centipede_interface.cc @@ -167,7 +167,8 @@ BinaryInfo PopulateBinaryInfoAndSavePCsIfNecessary( LOG(INFO) << "Serializing binary info to: " << binary_info_dir; binary_info.Write(binary_info_dir); } - if (binary_info.uses_legacy_trace_pc_instrumentation) { + if (binary_info.uses_legacy_trace_pc_instrumentation && + !binary_info.pc_table.empty()) { pcs_file_path = std::filesystem::path(TemporaryLocalDirPath()) / "pcs"; SavePCTableToFile(binary_info.pc_table, pcs_file_path); } @@ -423,21 +424,35 @@ void DeduplicateAndStoreNewCrashes( } // Seeds the corpus files in `env.workdir` with the previously distilled corpus -// files from `src_dir`. +// files from `coverage_dir` (if non-empty) SeedCorpusConfig GetSeedCorpusConfig(const Environment &env, - std::string_view src_dir) { + std::string_view regression_dir, + std::string_view coverage_dir) { const WorkDir workdir{env}; + std::vector sources; + if (!regression_dir.empty()) { + sources.push_back(SeedCorpusSource{ + .dir_glob = std::string(regression_dir), + .num_recent_dirs = 1, + .individual_input_rel_glob = "*", + .sampled_fraction_or_count = 1.0f, + }); + } + if (!coverage_dir.empty()) { + sources.push_back(SeedCorpusSource{ + .dir_glob = std::string(coverage_dir), + .num_recent_dirs = 1, + // We're using the previously distilled corpus files as seeds. + .shard_rel_glob = + std::filesystem::path{ + workdir.DistilledCorpusFilePaths().AllShardsGlob()} + .filename(), + .individual_input_rel_glob = "*", + .sampled_fraction_or_count = 1.0f, + }); + } return { - .sources = {SeedCorpusSource{ - .dir_glob = std::string(src_dir), - .num_recent_dirs = 1, - // We're using the previously distilled corpus files as seeds. - .shard_rel_glob = - std::filesystem::path{ - workdir.DistilledCorpusFilePaths().AllShardsGlob()} - .filename(), - .sampled_fraction_or_count = 1.0f, - }}, + .sources = std::move(sources), .destination = { .dir_path = env.workdir, @@ -505,10 +520,31 @@ int UpdateCorpusDatabaseForFuzzTests( absl::FormatTime("%Y-%m-%d-%H-%M-%S", absl::Now(), absl::UTCTimeZone()); return stamp; }(); + const auto fuzz_tests_to_run = [fuzztest_config, test_shard_index, + total_test_shards] { + if (getenv("FUZZTEST_CENTIPEDE_BINARY") == nullptr) { + CHECK(fuzztest_config.fuzz_tests_in_current_shard.size() == 1) + << "Must select exactly one fuzz test when running in the unified " + "exeuction model."; + return fuzztest_config.fuzz_tests_in_current_shard; + } + std::vector results; + for (int i = 0; i < fuzztest_config.fuzz_tests.size(); ++i) { + if (i % total_test_shards == test_shard_index) { + results.push_back(fuzztest_config.fuzz_tests[i]); + } + } + return results; + }(); + + const bool is_workdir_separate = !env.workdir.empty(); // the full workdir paths will be formed by appending the fuzz test names to // the base workdir path. const auto base_workdir_path = - corpus_database_path / absl::StrFormat("workdir.%03d", test_shard_index); + is_workdir_separate + ? std::filesystem::path(env.workdir) + : corpus_database_path / + absl::StrFormat("workdir.%03d", test_shard_index); // There's no point in saving the binary info to the workdir, since the // workdir is deleted at the end. env.save_binary_info = false; @@ -523,9 +559,8 @@ int UpdateCorpusDatabaseForFuzzTests( // Find the last index of a fuzz test for which we already have a workdir. bool is_resuming = false; int resuming_fuzztest_idx = 0; - for (int i = 0; i < fuzztest_config.fuzz_tests.size(); ++i) { - if (i % total_test_shards != test_shard_index) continue; - env.workdir = base_workdir_path / fuzztest_config.fuzz_tests[i]; + for (int i = 0; i < fuzz_tests_to_run.size(); ++i) { + env.workdir = base_workdir_path / fuzz_tests_to_run[i]; // Check the existence of the coverage path to not only make sure the // workdir exists, but also that it was created for the same binary as in // this run. @@ -536,19 +571,17 @@ int UpdateCorpusDatabaseForFuzzTests( } LOG_IF(INFO, is_resuming) << "Resuming from the fuzz test " - << fuzztest_config.fuzz_tests[resuming_fuzztest_idx] + << fuzz_tests_to_run[resuming_fuzztest_idx] << " (index: " << resuming_fuzztest_idx << ")"; // Step 3: Iterate over the fuzz tests and run them. const std::string binary = env.binary; - for (int i = resuming_fuzztest_idx; i < fuzztest_config.fuzz_tests.size(); - ++i) { - if (i % total_test_shards != test_shard_index) continue; + for (int i = resuming_fuzztest_idx; i < fuzz_tests_to_run.size(); ++i) { ReportErrorWhenNotEnoughTimeToRunEverything( start_time, fuzztest_config.time_limit, /*executed_tests_in_shard=*/i / total_test_shards, fuzztest_config.fuzz_tests.size(), total_test_shards); - env.workdir = base_workdir_path / fuzztest_config.fuzz_tests[i]; + env.workdir = base_workdir_path / fuzz_tests_to_run[i]; if (RemotePathExists(env.workdir) && !is_resuming) { // This could be a workdir from a failed run that used a different version // of the binary. We delete it so that we don't have to deal with the @@ -559,21 +592,31 @@ int UpdateCorpusDatabaseForFuzzTests( CHECK_OK(RemoteMkdir( workdir.CoverageDirPath())); // Implicitly creates the workdir - // Seed the fuzzing session with the latest coverage corpus from the - // previous fuzzing session. const std::filesystem::path fuzztest_db_path = - corpus_database_path / fuzztest_config.fuzz_tests[i]; + corpus_database_path / fuzz_tests_to_run[i]; + const std::filesystem::path regression_dir = + fuzztest_db_path / "regression"; const std::filesystem::path coverage_dir = fuzztest_db_path / "coverage"; - if (RemotePathExists(coverage_dir.c_str()) && !is_resuming) { - CHECK_OK(GenerateSeedCorpusFromConfig( - GetSeedCorpusConfig(env, coverage_dir.c_str()), env.binary_name, - env.binary_hash)); + + // Seed the fuzzing session with the latest coverage corpus and regression + // inputs from the previous fuzzing session. + if (!is_resuming) { + if (const auto status = GenerateSeedCorpusFromConfig( + GetSeedCorpusConfig( + env, regression_dir.c_str(), + fuzztest_config.replay_corpus ? coverage_dir.c_str() : ""), + env.binary_name, env.binary_hash); + !status.ok()) { + LOG(ERROR) << "Got error while generating the seed corpus: " << status; + } } - // TODO: b/338217594 - Call the FuzzTest binary in a flag-agnostic way. - constexpr std::string_view kFuzzTestFuzzFlag = "--fuzz="; - env.binary = absl::StrCat(binary, " ", kFuzzTestFuzzFlag, - fuzztest_config.fuzz_tests[i]); + if (getenv("FUZZTEST_CENTIPEDE_BINARY")) { + // TODO: b/338217594 - Call the FuzzTest binary in a flag-agnostic way. + constexpr std::string_view kFuzzTestFuzzFlag = "--fuzz="; + env.binary = + absl::StrCat(binary, " ", kFuzzTestFuzzFlag, fuzz_tests_to_run[i]); + } absl::Duration time_limit = fuzztest_config.GetTimeLimitPerTest(); absl::Duration time_spent = absl::ZeroDuration(); @@ -585,8 +628,8 @@ int UpdateCorpusDatabaseForFuzzTests( } is_resuming = false; - LOG(INFO) << "Fuzzing " << fuzztest_config.fuzz_tests[i] << " for " - << time_limit << "\n\tTest binary: " << env.binary; + LOG(INFO) << "Fuzzing " << fuzz_tests_to_run[i] << " for " << time_limit + << "\n\tTest binary: " << env.binary; const absl::Time start_time = absl::Now(); ClearEarlyStopRequestAndSetStopTime(/*stop_time=*/start_time + time_limit); @@ -597,7 +640,7 @@ int UpdateCorpusDatabaseForFuzzTests( record_fuzzing_time.Stop(); if (!stats_root_path.empty()) { - const auto stats_dir = stats_root_path / fuzztest_config.fuzz_tests[i]; + const auto stats_dir = stats_root_path / fuzz_tests_to_run[i]; CHECK_OK(RemoteMkdir(stats_dir.c_str())); CHECK_OK(RemotePathRename( workdir.FuzzingStatsPath(), @@ -605,6 +648,8 @@ int UpdateCorpusDatabaseForFuzzTests( .c_str())); } + if (fuzztest_config.replay_only || is_workdir_separate) continue; + // Distill and store the coverage corpus. Distill(env); if (RemotePathExists(coverage_dir.c_str())) { @@ -639,6 +684,28 @@ int UpdateCorpusDatabaseForFuzzTests( return EXIT_SUCCESS; } +absl::Status ListCrashIdsToFile( + const fuzztest::internal::Configuration &target_config, + std::string_view output_file) { + std::vector results; + std::vector crash_path_strs; + const auto glob = std::filesystem::path(target_config.corpus_database) / + target_config.binary_identifier / "*" / "crashing" / "*"; + const absl::Status match_status = + RemoteGlobMatch(glob.string(), crash_path_strs); + if (!match_status.ok() && !absl::IsNotFound(match_status)) + return match_status; + for (const auto &crash_path_str : crash_path_strs) { + const std::filesystem::path crash_path = {crash_path_str}; + const std::string crash_id = crash_path.filename(); + const std::string test_name = + crash_path.parent_path().parent_path().filename(); + results.push_back(absl::StrCat(test_name, "/", crash_id)); + } + CHECK_OK(RemoteFileSetContents(output_file, absl::StrJoin(results, "\n"))); + return absl::OkStatus(); +} + } // namespace int CentipedeMain(const Environment &env, @@ -694,6 +761,60 @@ int CentipedeMain(const Environment &env, CHECK_OK(target_config.status()) << "Failed to deserialize target configuration"; if (!target_config->corpus_database.empty()) { + if (!env.list_crash_ids_to_file.empty()) { + CHECK_OK( + ListCrashIdsToFile(*target_config, env.list_crash_ids_to_file)); + return EXIT_SUCCESS; + } else if (!env.run_crash_id.empty()) { + CHECK(target_config->fuzz_tests_in_current_shard.size() == 1) + << "Expecting exactly one test for run_crash_id"; + const auto crash_dir = + std::filesystem::path(target_config->corpus_database) / + target_config->binary_identifier / + target_config->fuzz_tests_in_current_shard[0] / "crashing"; + if (env.fetch_crash_to_file.empty()) { + const WorkDir workdir{env}; + CHECK_OK(GenerateSeedCorpusFromConfig( + SeedCorpusConfig{ + .sources = {{ + .dir_glob = crash_dir.string(), + .num_recent_dirs = 1, + .individual_input_rel_glob = env.run_crash_id, + .sampled_fraction_or_count = 1.0f, + }}, + .destination = + {.dir_path = env.workdir, + .shard_rel_glob = + std::filesystem::path{ + workdir.CorpusFilePaths().AllShardsGlob()} + .filename(), + .shard_index_digits = WorkDir::kDigitsInShardIndex, + .num_shards = 1}, + }, + env.binary_name, env.binary_hash)); + return Fuzz(env, {}, "", callbacks_factory); + } else { + std::string crash_contents; + const auto read_status = RemoteFileGetContents( + (crash_dir / env.run_crash_id).string(), crash_contents); + if (!read_status.ok()) { + LOG(ERROR) << "Failed reading the crash " << env.run_crash_id + << " from " << crash_dir.string() << ": " + << read_status; + return EXIT_FAILURE; + } + const auto write_status = + RemoteFileSetContents(env.fetch_crash_to_file, crash_contents); + if (!write_status.ok()) { + LOG(ERROR) << "Failed write the crash " << env.run_crash_id + << " to " << env.fetch_crash_to_file << ": " + << write_status; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; + } + } + const auto time_limit_per_test = target_config->GetTimeLimitPerTest(); CHECK(time_limit_per_test < absl::InfiniteDuration()) << "Updating corpus database requires specifying time limit per " diff --git a/centipede/environment.h b/centipede/environment.h index 751fad9e..4bd48042 100644 --- a/centipede/environment.h +++ b/centipede/environment.h @@ -127,6 +127,9 @@ struct Environment { bool first_corpus_dir_output_only = false; // If set, load/merge shards without fuzzing new inputs. bool load_shards_only = false; + std::string list_crash_ids_to_file; + std::string run_crash_id; + std::string fetch_crash_to_file; // Command line-related fields ----------------------------------------------- diff --git a/centipede/runner.cc b/centipede/runner.cc index 31950352..d6538b91 100644 --- a/centipede/runner.cc +++ b/centipede/runner.cc @@ -283,6 +283,7 @@ __attribute__((noinline)) void CheckStackLimit(uintptr_t sp) { // Check for the stack limit only if sp is inside the stack region. if (stack_limit > 0 && tls.stack_region_low && tls.top_frame_sp - sp > stack_limit) { + if (state.input_start_time == 0) return; if (stack_limit_exceeded.test_and_set()) return; fprintf(stderr, "========= Stack limit exceeded: %" PRIuPTR @@ -614,7 +615,9 @@ static void RunOneInput(const uint8_t *data, size_t size, int target_return_value = callbacks.Execute({data, size}) ? 0 : -1; state.stats.exec_time_usec = UsecSinceLast(); CheckWatchdogLimits(); - PostProcessCoverage(target_return_value); + if (centipede::state.input_start_time.exchange(0) != 0) { + PostProcessCoverage(target_return_value); + } state.stats.post_time_usec = UsecSinceLast(); state.stats.peak_rss_mb = GetPeakRSSMb(); } @@ -1235,7 +1238,9 @@ extern "C" void CentipedePrepareProcessing() { extern "C" void CentipedeFinalizeProcessing() { centipede::CheckWatchdogLimits(); - centipede::PostProcessCoverage(/*target_return_value=*/0); + if (centipede::state.input_start_time.exchange(0) != 0) { + centipede::PostProcessCoverage(/*target_return_value=*/0); + } } extern "C" size_t CentipedeGetExecutionResult(uint8_t *data, size_t capacity) { diff --git a/centipede/runner_interface.h b/centipede/runner_interface.h index 3227cc47..7fd75811 100644 --- a/centipede/runner_interface.h +++ b/centipede/runner_interface.h @@ -103,6 +103,9 @@ extern "C" void CentipedeEndExecutionBatch(); extern "C" void CentipedePrepareProcessing(); // Finalizes the processing of an input and stores the state internally. +// +// For tool integration, it can be called inside `RunnerCallbacks::Execute()` to +// finalize the execution early before extra cleanups. extern "C" void CentipedeFinalizeProcessing(); // Retrieves the execution results (including coverage information) after diff --git a/centipede/seed_corpus_maker_lib.cc b/centipede/seed_corpus_maker_lib.cc index 677ebc4c..4ad20f78 100644 --- a/centipede/seed_corpus_maker_lib.cc +++ b/centipede/seed_corpus_maker_lib.cc @@ -36,6 +36,7 @@ #include #include +#include "absl/container/flat_hash_set.h" #include "absl/log/check.h" #include "absl/log/log.h" #include "absl/random/random.h" @@ -115,21 +116,54 @@ absl::Status SampleSeedCorpusElementsFromSource( // LOG(INFO) << "Selected " << src_dirs.size() << " corpus dir(s)"; } - // Find all the corpus shard files in the found dirs. + // Find all the corpus shard and individual input files in the found dirs. std::vector corpus_shard_fnames; + std::vector individual_input_fnames; for (const auto& dir : src_dirs) { - const std::string shards_glob = fs::path{dir} / source.shard_rel_glob; - // NOTE: `RemoteGlobMatch` appends to the output list. - const auto prev_num_shards = corpus_shard_fnames.size(); - RETURN_IF_NOT_OK(RemoteGlobMatch(shards_glob, corpus_shard_fnames)); - LOG(INFO) << "Found " << (corpus_shard_fnames.size() - prev_num_shards) - << " shard(s) matching " << shards_glob; + absl::flat_hash_set current_corpus_shard_fnames; + if (!source.shard_rel_glob.empty()) { + std::vector matched_fnames; + const std::string glob = fs::path{dir} / source.shard_rel_glob; + const auto match_status = RemoteGlobMatch(glob, matched_fnames); + if (!match_status.ok() && !absl::IsNotFound(match_status)) { + LOG(ERROR) << "Got error when glob-matching in " << dir << ": " + << match_status; + } else { + current_corpus_shard_fnames.insert(matched_fnames.begin(), + matched_fnames.end()); + corpus_shard_fnames.insert(corpus_shard_fnames.end(), + matched_fnames.begin(), + matched_fnames.end()); + LOG(INFO) << "Found " << matched_fnames.size() << " shard(s) matching " + << glob; + } + } + if (!source.individual_input_rel_glob.empty()) { + std::vector matched_fnames; + const std::string glob = fs::path{dir} / source.individual_input_rel_glob; + const auto match_status = RemoteGlobMatch(glob, matched_fnames); + if (!match_status.ok() && !absl::IsNotFound(match_status)) { + LOG(ERROR) << "Got error when glob-matching in " << dir << ": " + << match_status; + } else { + size_t num_added_individual_inputs = 0; + for (auto& fname : matched_fnames) { + if (current_corpus_shard_fnames.contains(fname)) continue; + if (RemotePathIsDirectory(fname)) continue; + ++num_added_individual_inputs; + individual_input_fnames.push_back(std::move(fname)); + } + LOG(INFO) << "Found " << num_added_individual_inputs + << " individual input(s) with glob " << glob; + } + } } - LOG(INFO) << "Found " << corpus_shard_fnames.size() - << " shard(s) total in source " << source.dir_glob; + LOG(INFO) << "Found " << corpus_shard_fnames.size() << " shard(s) and " + << individual_input_fnames.size() + << " individual input(s) total in source " << source.dir_glob; - if (corpus_shard_fnames.empty()) { + if (corpus_shard_fnames.empty() && individual_input_fnames.empty()) { LOG(WARNING) << "Skipping empty source " << source.dir_glob; return absl::OkStatus(); } @@ -140,10 +174,12 @@ absl::Status SampleSeedCorpusElementsFromSource( // const auto num_shards = corpus_shard_fnames.size(); std::vector src_elts_per_shard(num_shards); std::vector src_elts_with_features_per_shard(num_shards, 0); + InputAndFeaturesVec src_elts; { constexpr int kMaxReadThreads = 32; - ThreadPool threads{std::min(kMaxReadThreads, num_shards)}; + ThreadPool threads{std::min( + kMaxReadThreads, std::max(num_shards, individual_input_fnames.size()))}; for (int shard = 0; shard < num_shards; ++shard) { const auto& corpus_fname = corpus_shard_fnames[shard]; @@ -193,11 +229,27 @@ absl::Status SampleSeedCorpusElementsFromSource( // threads.Schedule(read_shard); } + + RPROF_SNAPSHOT_AND_LOG("Done reading shards"); + + src_elts.resize(individual_input_fnames.size()); + for (size_t index = 0; index < individual_input_fnames.size(); ++index) { + threads.Schedule([index, &individual_input_fnames, &src_elts] { + ByteArray input; + const auto& path = individual_input_fnames[index]; + const auto read_status = RemoteFileGetContents(path, input); + if (!read_status.ok()) { + LOG(WARNING) << "Skipping individual input path " << path + << " due to read error: " << read_status; + return; + } + src_elts[index] = {std::move(input), {}}; + }); + } } RPROF_SNAPSHOT_AND_LOG("Done reading"); - InputAndFeaturesVec src_elts; size_t src_num_features = 0; for (int s = 0; s < num_shards; ++s) { @@ -217,6 +269,16 @@ absl::Status SampleSeedCorpusElementsFromSource( // RPROF_SNAPSHOT_AND_LOG("Done merging"); + // Remove empty inputs possibly due to read errors. + auto remove_it = + std::remove_if(src_elts.begin(), src_elts.end(), + [](const auto& elt) { return std::get<0>(elt).empty(); }); + if (remove_it != src_elts.end()) { + LOG(WARNING) << "Removed " << std::distance(remove_it, src_elts.end()) + << " empty inputs"; + src_elts.erase(remove_it, src_elts.end()); + } + LOG(INFO) << "Read total of " << src_elts.size() << " elements (" << src_num_features << " with features) from source " << source.dir_glob; diff --git a/centipede/seed_corpus_maker_lib.h b/centipede/seed_corpus_maker_lib.h index 68604fdb..5c3bb1c6 100644 --- a/centipede/seed_corpus_maker_lib.h +++ b/centipede/seed_corpus_maker_lib.h @@ -33,11 +33,16 @@ namespace centipede { // Native struct used by the seed corpus library for seed corpus source. // // TODO(b/362576261): Currently this is mirroring the `proto::SeedCorpusSource` -// proto. But in the future it may change with the core seeding API. +// proto. But in the future it may change with the core seeding API - any +// difference is commented below. struct SeedCorpusSource { std::string dir_glob; uint32_t num_recent_dirs; std::string shard_rel_glob; + // If non-empty, will be used to glob the individual input files (with one + // input in each file) in the source dirs. Any files matching `shard_rel_glob` + // will be skipped. + std::string individual_input_rel_glob; std::variant sampled_fraction_or_count; }; diff --git a/centipede/seed_corpus_maker_lib_test.cc b/centipede/seed_corpus_maker_lib_test.cc index a2e2b124..4064c3e3 100644 --- a/centipede/seed_corpus_maker_lib_test.cc +++ b/centipede/seed_corpus_maker_lib_test.cc @@ -21,13 +21,17 @@ #include // NOLINT #include #include +#include #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "absl/log/check.h" #include "absl/strings/str_cat.h" #include "./centipede/feature.h" #include "./centipede/workdir.h" +#include "./common/defs.h" #include "./common/logging.h" // IWYU pragma: keep +#include "./common/remote_file.h" #include "./common/test_util.h" namespace centipede { @@ -36,6 +40,7 @@ namespace { namespace fs = std::filesystem; using ::testing::IsSubsetOf; +using ::testing::IsSupersetOf; inline constexpr auto kIdxDigits = WorkDir::kDigitsInShardIndex; @@ -178,5 +183,69 @@ TEST(SeedCorpusMakerLibTest, RoundTripWriteReadWrite) { } } +TEST(SeedCorpusMakerLibTest, LoadsBothIndividualInputsAndShardsFromSource) { + const fs::path test_dir = GetTestTempDir(test_info_->name()); + chdir(test_dir.c_str()); + + const InputAndFeaturesVec kShardedInputs = { + {{0}, {}}, + {{1}, {feature_domains::kNoFeature}}, + {{0, 1}, {0x11, 0x23}}, + }; + constexpr std::string_view kCovBin = "bin"; + constexpr std::string_view kCovHash = "hash"; + constexpr std::string_view kRelDir = "dir/foo"; + + const std::vector kIndividualInputs = { + {0, 1, 2}, + {0, 1, 2, 3}, + // Empty input expected to be not in the sample result. + {}}; + // Write sharded inputs. + { + constexpr size_t kNumShards = 2; + const SeedCorpusDestination destination = { + .dir_path = std::string(kRelDir), + .shard_rel_glob = absl::StrCat("distilled-", kCovBin, ".*"), + .shard_index_digits = kIdxDigits, + .num_shards = kNumShards, + }; + CHECK_OK(WriteSeedCorpusElementsToDestination( // + kShardedInputs, kCovBin, kCovHash, destination)); + const std::string workdir = (test_dir / kRelDir).c_str(); + ASSERT_NO_FATAL_FAILURE(VerifyShardsExist( // + workdir, kCovBin, kCovHash, kNumShards, ShardType::kDistilled)); + } + + // Write individual inputs + for (int i = 0; i < kIndividualInputs.size(); ++i) { + const auto path = std::filesystem::path(test_dir) / kRelDir / + absl::StrCat("individual_input_", i); + CHECK_OK(RemoteFileSetContents(path.string(), kIndividualInputs[i])); + } + + // Test that sharded and individual inputs matches what we wrote. + { + InputAndFeaturesVec elements; + ASSERT_OK(SampleSeedCorpusElementsFromSource( // + SeedCorpusSource{ + .dir_glob = std::string(kRelDir), + .num_recent_dirs = 1, + .shard_rel_glob = absl::StrCat("distilled-", kCovBin, ".*"), + // Intentionally try to match the shard files and test if they will + // be read as individual inputs. + .individual_input_rel_glob = "*", + .sampled_fraction_or_count = 1.0f, + }, + kCovBin, kCovHash, elements)); + EXPECT_EQ(elements.size(), 5); // Non-empty inputs + EXPECT_THAT(elements, IsSupersetOf(kShardedInputs)); + EXPECT_THAT(elements, IsSupersetOf(InputAndFeaturesVec{ + {{0, 1, 2}, {}}, + {{0, 1, 2, 3}, {}}, + })); + } +} + } // namespace } // namespace centipede diff --git a/common/remote_file_oss.cc b/common/remote_file_oss.cc index 2ea73afe..646d5fa6 100644 --- a/common/remote_file_oss.cc +++ b/common/remote_file_oss.cc @@ -349,6 +349,10 @@ absl::Status RemoteGlobMatch(std::string_view glob, if (int ret = ::glob(std::string{glob}.c_str(), GLOB_TILDE, HandleGlobError, &glob_ret); ret != 0) { + if (ret == GLOB_NOMATCH) { + return absl::NotFoundError(absl::StrCat( + "glob() returned NOMATCH for pattern: ", std::string(glob))); + } return absl::UnknownError(absl::StrCat( "glob() failed, pattern: ", std::string(glob), ", returned: ", ret)); } diff --git a/e2e_tests/corpus_database_test.cc b/e2e_tests/corpus_database_test.cc index 4b46a194..438a2954 100644 --- a/e2e_tests/corpus_database_test.cc +++ b/e2e_tests/corpus_database_test.cc @@ -68,15 +68,13 @@ class UpdateCorpusDatabaseTest : public testing::Test { temp_dir_ = new TempDir(); - auto [status, std_out, std_err] = RunBinary( - CentipedePath(), - {.flags = {{"binary", - absl::StrJoin({GetCorpusDatabaseTestingBinaryPath(), - CreateFuzzTestFlag("corpus_database", - GetCorpusDatabasePath()), - CreateFuzzTestFlag("fuzz_for", "30s"), - CreateFuzzTestFlag("jobs", "2")}, - /*separator=*/" ")}}}); + auto [status, std_out, std_err] = RunBinaryMaybeWithCentipede( + GetCorpusDatabaseTestingBinaryPath(), + {.fuzztest_flags = { + {"corpus_database", GetCorpusDatabasePath()}, + {"fuzz_for", "30s"}, + {"jobs", "2"}, + }}); *centipede_std_out_ = std::move(std_out); *centipede_std_err_ = std::move(std_err); @@ -96,6 +94,31 @@ class UpdateCorpusDatabaseTest : public testing::Test { static absl::string_view GetCentipedeStdErr() { return *centipede_std_err_; } + static RunResults RunBinaryMaybeWithCentipede(absl::string_view binary_path, + const RunOptions &options) { +#ifndef FUZZTEST_USE_CENTIPEDE_SINGLE_PROCESS + return RunBinary(binary_path, options); +#else + RunOptions actual_options; + actual_options.env = options.env; + actual_options.timeout = options.timeout; + std::vector binary_args; + binary_args.push_back(std::string(binary_path)); + for (const auto &[key, value] : options.fuzztest_flags) { + binary_args.push_back(CreateFuzzTestFlag(key, value)); + } + for (const auto &[key, value] : options.flags) { + binary_args.push_back(absl::StrCat("--", key, "=", value)); + } + actual_options.flags = { + {"binary", absl::StrJoin(binary_args, " ")}, + // Disable symbolization to more quickly get to fuzzing. + {"symbolizer_path", ""}, + }; + return RunBinary(CentipedePath(), actual_options); +#endif + } + private: static TempDir *temp_dir_; static absl::NoDestructor centipede_std_out_; @@ -132,17 +155,13 @@ TEST_F(UpdateCorpusDatabaseTest, ResumedFuzzTestRunsForRemainingTime) { TempDir corpus_database; // 1st run that gets interrupted. - auto [fst_status, fst_std_out, fst_std_err] = RunBinary( - CentipedePath(), - {.flags = {{"binary", - absl::StrJoin({GetCorpusDatabaseTestingBinaryPath(), - CreateFuzzTestFlag("corpus_database", - corpus_database.dirname()), - CreateFuzzTestFlag("fuzz_for", "300s")}, - /*separator=*/" ")}, - // Disable symbolization to more quickly get to fuzzing. - {"symbolizer_path", ""}}, - // Stop the binary with SIGTERM before the fuzzing is done. + auto [fst_status, fst_std_out, fst_std_err] = RunBinaryMaybeWithCentipede( + GetCorpusDatabaseTestingBinaryPath(), + {.fuzztest_flags = + { + {"corpus_database", corpus_database.dirname()}, + {"fuzz_for", "300s"}, + }, .timeout = absl::Seconds(10)}); ASSERT_THAT(fst_status, Eq(Signal(SIGTERM))); @@ -153,16 +172,13 @@ TEST_F(UpdateCorpusDatabaseTest, ResumedFuzzTestRunsForRemainingTime) { ASSERT_TRUE(WriteFile(*fuzzing_time_file, "299s")); // 2nd run that resumes the fuzzing. - auto [snd_status, snd_std_out, snd_std_err] = RunBinary( - CentipedePath(), - {.flags = {{"binary", - absl::StrJoin({GetCorpusDatabaseTestingBinaryPath(), - CreateFuzzTestFlag("corpus_database", - corpus_database.dirname()), - CreateFuzzTestFlag("fuzz_for", "300s")}, - /*separator=*/" ")}, - // Disable symbolization to more quickly get to fuzzing. - {"symbolizer_path", ""}}, + auto [snd_status, snd_std_out, snd_std_err] = RunBinaryMaybeWithCentipede( + GetCorpusDatabaseTestingBinaryPath(), + {.fuzztest_flags = + { + {"corpus_database", corpus_database.dirname()}, + {"fuzz_for", "300s"}, + }, .timeout = absl::Seconds(10)}); EXPECT_THAT( diff --git a/e2e_tests/functional_test.cc b/e2e_tests/functional_test.cc index 33225640..ee013ac9 100644 --- a/e2e_tests/functional_test.cc +++ b/e2e_tests/functional_test.cc @@ -19,7 +19,9 @@ #include #include // NOLINT #include +#include #include +#include #include #include #include @@ -75,6 +77,49 @@ absl::flat_hash_map WithTestSanitizerOptions( return env; } +void ExpectTargetAbort(TerminationStatus status, absl::string_view std_err) { +#ifdef FUZZTEST_USE_CENTIPEDE + EXPECT_THAT(status, Ne(ExitCode(0))); + EXPECT_TRUE( + RE2::PartialMatch(std_err, absl::StrCat("Exit code\\s*:\\s*", SIGABRT))) + << std_err; +#else + EXPECT_THAT(status, Eq(Signal(SIGABRT))); +#endif +} + +int CountSubstrs(absl::string_view haystack, absl::string_view needle) { + int count = 0; + while (true) { + size_t pos = haystack.find(needle); + if (pos == haystack.npos) return count; + ++count; + haystack.remove_prefix(pos + needle.size()); + } +} + +// Counts the number of times the target binary has been run. Needed because +// Centipede runs the binary multiple times. +int CountTargetRuns(absl::string_view std_err) { + return CountSubstrs(std_err, "FuzzTest functional test target run"); +} + +std::string TraceMessageForRunResult(TerminationStatus status, + absl::string_view std_out, + absl::string_view std_err) { + std::ostringstream ss; + ss << "EXIT STATUS: " << status + << "\n==== STDOUT " + "====================================================================\n" + << std_out + << "\n==== STDERR " + "====================================================================\n" + << std_err + << "\n====================================================================" + "============\n"; + return ss.str(); +} + class UnitTestModeTest : public ::testing::Test { protected: RunResults Run( @@ -84,7 +129,8 @@ class UnitTestModeTest : public ::testing::Test { const absl::flat_hash_map& fuzzer_flags = {}) { return RunBinary( BinaryPath(target_binary), - {.flags = {{GTEST_FLAG_PREFIX_ "filter", std::string(test_filter)}}, + {.flags = {{GTEST_FLAG_PREFIX_ "filter", std::string(test_filter)}, + {"symbolize_stacktrace", "0"}}, .fuzztest_flags = fuzzer_flags, .env = WithTestSanitizerOptions(env)}); } @@ -111,7 +157,10 @@ TEST_F(UnitTestModeTest, InvalidSeedsAreSkippedAndReported) { } TEST_F(UnitTestModeTest, CorpusIsMutatedInUnitTestMode) { - auto [status, std_out, std_err] = Run("MySuite.PassesString"); + auto [status, std_out, std_err] = + Run("MySuite.PassesString", kDefaultTargetBinary, /*env=*/{}, + /*fuzzer_flags=*/{{"print_subprocess_log", "true"}}); + SCOPED_TRACE(TraceMessageForRunResult(status, std_out, std_err)); EXPECT_THAT(std_err, HasSubstr("==<>==")); EXPECT_THAT(std_err, HasSubstr("==<>==")); EXPECT_THAT(std_err, HasSubstr("==<>==")); @@ -128,16 +177,6 @@ TEST_F(UnitTestModeTest, UnitTestModeLimitsNumberOfIterationsByWallTime) { EXPECT_THAT(status, Eq(ExitCode(0))); } -int CountSubstrs(absl::string_view haystack, absl::string_view needle) { - int count = 0; - while (true) { - size_t pos = haystack.find(needle); - if (pos == haystack.npos) return count; - ++count; - haystack.remove_prefix(pos + needle.size()); - } -} - RE2 MakeReproducerRegex(absl::string_view suite_name, absl::string_view test_name, absl::string_view args) { return RE2( @@ -175,6 +214,7 @@ MATCHER_P3(HasReproducerTest, suite_name, test_name, args, "") { void GoogleTestExpectationsDontAbortInUnitTestModeImpl( const RunResults& run_results) { const auto& [status, std_out, std_err] = run_results; + SCOPED_TRACE(TraceMessageForRunResult(status, std_out, std_err)); EXPECT_THAT(std_err, HasSubstr("argument 0: ")) << std_err; // We expect both to run without crashing. EXPECT_THAT(std_out, HasSubstr("[ FAILED ] MySuite.GoogleTestExpect")) @@ -207,18 +247,23 @@ TEST_F(UnitTestModeTest, TEST_F(UnitTestModeTest, GlobalEnvironmentGoesThroughCompleteLifecycle) { auto [status, std_out, std_err] = Run("MySuite.GoogleTestExpect"); + SCOPED_TRACE(TraceMessageForRunResult(status, std_out, std_err)); + EXPECT_GT(CountSubstrs(std_err, "<>"), + 0); EXPECT_EQ( - 1, CountSubstrs(std_err, "<>")); - EXPECT_EQ(1, CountSubstrs(std_err, "<>")); - EXPECT_EQ(1, CountSubstrs(std_err, "<>")); - EXPECT_EQ( - 1, CountSubstrs(std_err, "<>")); + CountSubstrs(std_err, "<>"), + CountSubstrs(std_err, "<>")); + EXPECT_GT(CountSubstrs(std_err, "<>"), 0); + EXPECT_EQ(CountSubstrs(std_err, "<>"), + CountSubstrs(std_err, "<>")); } TEST_F(UnitTestModeTest, FixtureGoesThroughCompleteLifecycle) { auto [status, std_out, std_err] = Run("FixtureTest.NeverFails"); - EXPECT_EQ(1, CountSubstrs(std_err, "<>")); - EXPECT_EQ(1, CountSubstrs(std_err, "<>")); + SCOPED_TRACE(TraceMessageForRunResult(status, std_out, std_err)); + EXPECT_GT(CountSubstrs(std_err, "<>"), 0); + EXPECT_EQ(CountSubstrs(std_err, "<>"), + CountSubstrs(std_err, "<>")); } TEST_F(UnitTestModeTest, @@ -231,24 +276,25 @@ TEST_F(UnitTestModeTest, TEST_F(UnitTestModeTest, GoogleTestPerFuzzTestFixtureInstantiatedOncePerFuzzTest) { auto [status, std_out, std_err] = - Run("CallCountPerFuzzTest.CallCountReachesAtLeastTen"); - EXPECT_EQ( - 1, CountSubstrs(std_err, "<>")); + Run("CallCountPerFuzzTest.CallCountPerFuzzTestEqualsToGlobalCount"); + SCOPED_TRACE(TraceMessageForRunResult(status, std_out, std_err)); + EXPECT_THAT(status, Eq(ExitCode(0))); } -TEST_F(UnitTestModeTest, GoogleTestStaticTestSuiteFunctionsCalledOnce) { +TEST_F(UnitTestModeTest, GoogleTestStaticTestSuiteFunctionsCalledInBalance) { auto [status, std_out, std_err] = - Run("CallCountPerFuzzTest.CallCountReachesAtLeastTen:" + Run("CallCountPerFuzzTest.CallCountPerFuzzTestEqualsToGlobalCount:" "CallCountPerFuzzTest.NeverFails"); - EXPECT_EQ(1, - CountSubstrs(std_err, "<>")); + EXPECT_GT(CountSubstrs(std_err, "<>"), + 0); EXPECT_EQ( - 1, CountSubstrs(std_err, "<>")); + CountSubstrs(std_err, "<>"), + CountSubstrs(std_err, "<>")); } TEST_F(UnitTestModeTest, GoogleTestWorksWithProtoExtensionsUsedInSeeds) { auto [status, std_out, std_err] = Run("MySuite.CheckProtoExtensions"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); EXPECT_THAT(std_err, HasSubstr("Uses proto extensions")); } @@ -265,7 +311,7 @@ TEST_F(UnitTestModeTest, RepeatedFieldsHaveMinSizeWhenInitialized) { TEST_F(UnitTestModeTest, OptionalProtoFieldCanHaveNoValue) { auto [status, std_out, std_err] = Run("MySuite.FailsWhenFieldI32HasNoValue"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(UnitTestModeTest, OptionalProtoFieldThatIsUnsetNeverHasValue) { @@ -309,26 +355,26 @@ TEST_F(UnitTestModeTest, TEST_F(UnitTestModeTest, RequiredProtoFieldThatIsNotAlwaysSetCanHaveNoValue) { auto [status, std_out, std_err] = Run("MySuite.FailsWhenRequiredEnumFieldHasNoValue"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); EXPECT_THAT(std_err, HasSubstr("cannot have null values")); } TEST_F(UnitTestModeTest, OptionalProtoFieldThatIsNotAlwaysSetCanHaveNoValue) { auto [status, std_out, std_err] = Run("MySuite.FailsWhenOptionalFieldU32HasNoValue"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(UnitTestModeTest, ProtobufOfMutatesTheProto) { auto [status, std_out, std_err] = Run("MySuite.FailsWhenI32IsSet"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); EXPECT_THAT(std_err, HasSubstr("The field i32 is set!")); } TEST_F(UnitTestModeTest, ProtobufEnumEqualsLabel4) { auto [status, std_out, std_err] = Run("MySuite.FailsIfProtobufEnumEqualsLabel4"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); EXPECT_THAT( std_err, HasSubstr("argument 0: fuzztest::internal::TestProtobuf::Label4")); @@ -336,7 +382,7 @@ TEST_F(UnitTestModeTest, ProtobufEnumEqualsLabel4) { TEST_F(UnitTestModeTest, WorksWithRecursiveStructs) { auto [status, std_out, std_err] = Run("MySuite.WorksWithRecursiveStructs"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); // Nullptr has multiple possible human-readable representations. EXPECT_THAT(std_err, AnyOf(HasSubstr("argument 0: LinkedList{0, 1}"), HasSubstr("argument 0: LinkedList{(nil), 1}"))); @@ -345,46 +391,46 @@ TEST_F(UnitTestModeTest, WorksWithRecursiveStructs) { TEST_F(UnitTestModeTest, WorksWithStructsWithConstructors) { auto [status, std_out, std_err] = Run("MySuite.WorksWithStructsWithConstructors"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); EXPECT_THAT(std_err, HasSubstr("argument 0: HasConstructor{1, \"abc\"}")); } TEST_F(UnitTestModeTest, WorksWithStructsWithEmptyTuples) { auto [status, std_out, std_err] = Run("MySuite.WorksWithStructsWithEmptyTuples"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); EXPECT_THAT(std_err, HasSubstr("argument 0: ContainsEmptyTuple{}")); } TEST_F(UnitTestModeTest, WorksWithEmptyStructs) { auto [status, std_out, std_err] = Run("MySuite.WorksWithEmptyStructs"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); EXPECT_THAT(std_err, HasSubstr("argument 0: Empty{}")); } TEST_F(UnitTestModeTest, WorksWithStructsWithEmptyFields) { auto [status, std_out, std_err] = Run("MySuite.WorksWithStructsWithEmptyFields"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); EXPECT_THAT(std_err, HasSubstr("argument 0: ContainsEmpty{Empty{}}")); } TEST_F(UnitTestModeTest, WorksWithEmptyInheritance) { auto [status, std_out, std_err] = Run("MySuite.WorksWithEmptyInheritance"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); EXPECT_THAT(std_err, HasSubstr("argument 0: Child{0, \"abc\"}")); } TEST_F(UnitTestModeTest, ArbitraryWorksWithEmptyInheritance) { auto [status, std_out, std_err] = Run("MySuite.ArbitraryWorksWithEmptyInheritance"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); EXPECT_THAT(std_err, HasSubstr("argument 0:")); } TEST_F(UnitTestModeTest, FlatMapCorrectlyPrintsValues) { auto [status, std_out, std_err] = Run("MySuite.FlatMapCorrectlyPrintsValues"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); // This is the argument to the output domain. EXPECT_THAT(std_err, HasSubstr("argument 0: {\"AAA\", \"BBB\"}")); // This is the argument to the input domain. @@ -393,24 +439,24 @@ TEST_F(UnitTestModeTest, FlatMapCorrectlyPrintsValues) { TEST_F(UnitTestModeTest, PrintsVeryLongInputsTrimmed) { auto [status, std_out, std_err] = Run("MySuite.LongInput"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); EXPECT_THAT(std_err, HasSubstr("65 ...")); EXPECT_THAT(std_err, HasSubstr("A ...")); } TEST_F(UnitTestModeTest, PropertyFunctionAcceptsTupleOfItsSingleParameter) { auto [status, std_out, std_err] = Run("MySuite.UnpacksTupleOfOne"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(UnitTestModeTest, PropertyFunctionAcceptsTupleOfItsThreeParameters) { auto [status, std_out, std_err] = Run("MySuite.UnpacksTupleOfThree"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(UnitTestModeTest, PropertyFunctionAcceptsTupleContainingTuple) { auto [status, std_out, std_err] = Run("MySuite.UnpacksTupleContainingTuple"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(UnitTestModeTest, ProtoFieldsCanBeAlwaysSet) { @@ -456,7 +502,7 @@ TEST_F( TEST_F(UnitTestModeTest, DetectsRecursiveStructureIfOptionalsSetByDefault) { auto [status, std_out, std_err] = Run("MySuite.FailsIfCantInitializeProto"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); EXPECT_THAT(std_err, HasSubstr("recursive fields")); } @@ -485,7 +531,7 @@ TEST_F(UnitTestModeTest, TEST_F(UnitTestModeTest, FailsWhenRepeatedFieldsSizeRangeIsInvalid) { auto [status, std_out, std_err] = Run("MySuite.FailsToInitializeIfRepeatedFieldsSizeRangeIsInvalid"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); EXPECT_THAT(std_err, HasSubstr("size range is not valid")); } @@ -498,7 +544,7 @@ TEST_F(UnitTestModeTest, UsesPolicyProvidedDefaultDomainForProtos) { TEST_F(UnitTestModeTest, ChecksTypeOfProvidedDefaultDomainForProtos) { auto [status, std_out, std_err] = Run("MySuite.FailsWhenWrongDefaultProtobufDomainIsProvided"); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); EXPECT_THAT(std_err, HasSubstr("does not match the expected message type")); } @@ -542,16 +588,17 @@ TEST_F(UnitTestModeTest, StackLimitWorks) { /*env=*/{}, /*fuzzer_flags=*/{{"stack_limit_kb", "1000"}}); EXPECT_THAT(std_err, HasSubstr("argument 0: ")); ExpectStackLimitExceededMessage(std_err, 1024000); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(UnitTestModeTest, RssLimitFlagWorks) { auto [status, std_out, std_err] = Run("MySuite.LargeHeapAllocation", kDefaultTargetBinary, /*env=*/{}, /*fuzzer_flags=*/{{"rss_limit_mb", "1024"}}); + SCOPED_TRACE(TraceMessageForRunResult(status, std_out, std_err)); EXPECT_THAT(std_err, HasSubstr("argument 0: ")); EXPECT_THAT(std_err, ContainsRegex(absl::StrCat("RSS limit exceeded"))); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(UnitTestModeTest, TimeLimitFlagWorks) { @@ -561,7 +608,7 @@ TEST_F(UnitTestModeTest, TimeLimitFlagWorks) { /*fuzzer_flags=*/{{"time_limit_per_input", "1s"}}); EXPECT_THAT(std_err, HasSubstr("argument 0: ")); EXPECT_THAT(std_err, ContainsRegex("Per-input timeout exceeded")); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(UnitTestModeTest, TestIsSkippedWhenRequestedInFixturePerTest) { @@ -569,6 +616,7 @@ TEST_F(UnitTestModeTest, TestIsSkippedWhenRequestedInFixturePerTest) { Run("SkippedTestFixturePerTest.SkippedTest", kDefaultTargetBinary, /*env=*/{}, /*fuzzer_flags=*/{{"time_limit_per_input", "1s"}}); + SCOPED_TRACE(TraceMessageForRunResult(status, std_out, std_err)); EXPECT_THAT(std_err, HasSubstr("Skipping SkippedTestFixturePerTest.SkippedTest")); EXPECT_THAT(std_err, Not(HasSubstr("SkippedTest is executed"))); @@ -588,7 +636,8 @@ TEST_F(UnitTestModeTest, InputsAreSkippedWhenRequestedInTests) { auto [status, std_out, std_err] = Run("MySuite.SkipInputs", kDefaultTargetBinary, /*env=*/{}, - /*fuzzer_flags=*/{{"time_limit_per_input", "1s"}}); + /*fuzzer_flags=*/ + {{"time_limit_per_input", "1s"}, {"print_subprocess_log", "true"}}); EXPECT_THAT(std_err, HasSubstr("Skipped input")); } @@ -714,14 +763,14 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, TEST_F(FuzzingModeCommandLineInterfaceTest, RunsAbortTestAndDetectsAbort) { auto [status, std_out, std_err] = RunWith({{"fuzz", "MySuite.Aborts"}}); EXPECT_THAT(std_err, HasSubstr("argument 0: ")); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(FuzzingModeCommandLineInterfaceTest, FuzzTestCanBeSelectedForFuzzingUsingSubstring) { auto [status, std_out, std_err] = RunWith({{"fuzz", "Abort"}}); EXPECT_THAT(std_err, HasSubstr("argument 0: ")); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(FuzzingModeCommandLineInterfaceTest, @@ -760,7 +809,7 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, ReproducerIsDumpedWhenEnvVarIsSet) { RunWith({{"fuzz", "MySuite.StringFast"}}, {{"FUZZTEST_REPRODUCERS_OUT_DIR", out_dir.dirname()}}); EXPECT_THAT(std_err, HasSubstr("argument 0: \"Fuzz")); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); auto replay_files = ReadFileOrDirectory(out_dir.dirname()); ASSERT_EQ(replay_files.size(), 1) << std_err; @@ -769,10 +818,10 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, ReproducerIsDumpedWhenEnvVarIsSet) { auto args = parsed->ToCorpus>(); EXPECT_THAT(args, Optional(FieldsAre(StartsWith("Fuzz")))) << std_err; EXPECT_THAT(std_err, - HasSubstr(absl::StrCat("Reproducer file was dumped at:\n", - replay_files[0].path))); - EXPECT_THAT(std_err, HasSubstr(absl::StrCat("--test_env=FUZZTEST_REPLAY=", - replay_files[0].path))); + AllOf(HasSubstr("Reproducer file was dumped at:"), + HasSubstr(replay_files[0].path), + HasSubstr(absl::StrCat("--test_env=FUZZTEST_REPLAY=", + replay_files[0].path)))); } TEST_F(FuzzingModeCommandLineInterfaceTest, SavesCorpusWhenEnvVarIsSet) { @@ -991,8 +1040,8 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, ReplayFile replay(std::in_place, std::tuple{10, 1979.125}); auto [status, std_out, std_err] = RunWith({{"fuzz", "MySuite.WithDomainClass"}}, replay.GetReplayEnv()); - EXPECT_THAT(std_err, HasSubstr("argument 0: 10")); - EXPECT_THAT(std_err, HasSubstr("argument 1: 1979.125")); + EXPECT_THAT(std_err, HasSubstr("argument 0: 10")) << std_err; + EXPECT_THAT(std_err, HasSubstr("argument 1: 1979.125")) << std_err; EXPECT_THAT(status, Ne(ExitCode(0))); } @@ -1025,6 +1074,10 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, MinimizerFindsSmallerInput) { TEST_F(FuzzingModeCommandLineInterfaceTest, FuzzerStatsArePrintedOnTermination) { +#ifdef FUZZTEST_USE_CENTIPEDE + GTEST_SKIP() << "Skip fuzzer stats test when running with Centipede because " + "stats are not printed."; +#endif auto [status, std_out, std_err] = RunWith({{"fuzz", "MySuite.PassesWithPositiveInput"}}, /*env=*/{}, @@ -1039,10 +1092,11 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, SilenceTargetWorking) { auto [status, std_out, std_err] = RunWith({{"fuzz", "MySuite.TargetPrintSomethingThenAbrt"}}, /*env=*/{{"FUZZTEST_SILENCE_TARGET", "1"}}); + SCOPED_TRACE(TraceMessageForRunResult(status, std_out, std_err)); EXPECT_THAT(std_out, Not(HasSubstr("Hello World from target stdout"))); EXPECT_THAT(std_err, HasSubstr("=== Fuzzing stats")); EXPECT_THAT(std_err, Not(HasSubstr("Hello World from target stderr"))); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(FuzzingModeCommandLineInterfaceTest, NonFatalFailureAllowsMinimization) { @@ -1057,7 +1111,7 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, NonFatalFailureAllowsMinimization) { // "larger" inputs also trigger the failure. EXPECT_THAT(std_err, HasSubstr("argument 0: \"0123\"")); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(FuzzingModeCommandLineInterfaceTest, GoogleTestHasCurrentTestInfo) { @@ -1074,7 +1128,7 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, ConfiguresStackLimitByFlag) { {"stack_limit_kb", "1000"}}); EXPECT_THAT(std_err, HasSubstr("argument 0: ")); ExpectStackLimitExceededMessage(std_err, 1024000); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(FuzzingModeCommandLineInterfaceTest, @@ -1089,7 +1143,7 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, "is going to be deprecated soon. Consider switching to ", CreateFuzzTestFlag("stack_limit_kb", ""), " flag."))); ExpectStackLimitExceededMessage(std_err, 512000); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(FuzzingModeCommandLineInterfaceTest, @@ -1100,15 +1154,17 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, {{"FUZZTEST_STACK_LIMIT", "512000"}}); EXPECT_THAT(std_err, HasSubstr("argument 0: ")); ExpectStackLimitExceededMessage(std_err, 512000); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(FuzzingModeCommandLineInterfaceTest, DoesNotPrintWarningForDisabledLimitFlagsByDefault) { auto [status, std_out, std_err] = - RunWith({{"fuzz", "MySuite.PassesWithPositiveInput"}}, + RunWith({{"fuzz", "MySuite.PassesWithPositiveInput"}, + {"print_subprocess_log", "true"}}, /*env=*/{}, - /*timeout=*/absl::Seconds(10)); + /*timeout=*/absl::Seconds(20)); + SCOPED_TRACE(TraceMessageForRunResult(status, std_out, std_err)); EXPECT_THAT(std_err, Not(HasSubstr("limit is specified but will be ignored"))); EXPECT_THAT(status, Eq(ExitCode(0))); @@ -1120,7 +1176,7 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, RssLimitFlagWorks) { /*env=*/{}, /*timeout=*/absl::Seconds(10)); EXPECT_THAT(std_err, HasSubstr("argument 0: ")); EXPECT_THAT(std_err, ContainsRegex(absl::StrCat("RSS limit exceeded"))); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } TEST_F(FuzzingModeCommandLineInterfaceTest, TimeLimitFlagWorks) { @@ -1129,7 +1185,7 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, TimeLimitFlagWorks) { /*env=*/{}); EXPECT_THAT(std_err, HasSubstr("argument 0: ")); EXPECT_THAT(std_err, ContainsRegex("Per-input timeout exceeded")); - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + ExpectTargetAbort(status, std_err); } // TODO: b/340232436 - Once fixed, remove this test since we will no longer need @@ -1138,7 +1194,7 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, RunsOnlyFuzzTests) { auto [status, std_out, std_err] = RunWith({{"fuzz_for", "1ns"}}, /*env=*/{}, /*timeout=*/absl::Seconds(10), "testdata/unit_test_and_fuzz_tests"); - + SCOPED_TRACE(TraceMessageForRunResult(status, std_out, std_err)); EXPECT_THAT(std_out, Not(HasSubstr("[ RUN ] UnitTest.AlwaysPasses"))); EXPECT_THAT(std_out, HasSubstr("[ RUN ] FuzzTest.AlwaysPasses")); EXPECT_THAT(std_out, HasSubstr("[ RUN ] FuzzTest.AlsoAlwaysPasses")); @@ -1169,14 +1225,16 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, CorpusDoesNotContainSkippedInputs) { // Although theoretically possible, it is extreme unlikely that the test would // find the crash without saving some corpus. auto [producer_status, producer_std_out, producer_std_err] = - RunWith({{"fuzz", "MySuite.SkipInputs"}, {"fuzz_for", "10s"}}, + RunWith({{"fuzz", "MySuite.SkipInputs"}, + {"fuzz_for", "10s"}, + {"print_subprocess_log", "true"}}, {{"FUZZTEST_TESTSUITE_OUT_DIR", corpus_dir.dirname()}}); ASSERT_THAT(producer_std_err, HasSubstr("Skipped input")); - auto [replayer_status, replayer_std_out, replayer_std_err] = - RunWith({{"fuzz", "MySuite.SkipInputs"}}, - {{"FUZZTEST_REPLAY", corpus_dir.dirname()}}); + auto [replayer_status, replayer_std_out, replayer_std_err] = RunWith( + {{"fuzz", "MySuite.SkipInputs"}, {"print_subprocess_log", "true"}}, + {{"FUZZTEST_REPLAY", corpus_dir.dirname()}}); EXPECT_THAT(replayer_std_err, Not(HasSubstr("Skipped input"))); } @@ -1200,14 +1258,16 @@ TEST_F(FuzzingModeCommandLineInterfaceTest, UsesCentipedeBinaryWhenEnvIsSet) { EXPECT_THAT(status, Eq(ExitCode(0))); } -struct ExecutionModelParam { - bool multi_process; +enum class ExecutionModelParam { + kSingleBinary, + kWithCentipedeBinary, }; std::vector GetAvailableExecutionModels() { - std::vector results = {{/*multi_process=*/false}}; + std::vector results = { + ExecutionModelParam::kSingleBinary}; #ifdef FUZZTEST_USE_CENTIPEDE - results.push_back({/*multi_process=*/true}); + results.push_back(ExecutionModelParam::kWithCentipedeBinary); #endif return results; } @@ -1233,34 +1293,30 @@ class FuzzingModeFixtureTest } RunResults Run(absl::string_view test_name, int iterations) { - if (GetParam().multi_process) { - TempDir workdir; - return RunBinary( - CentipedePath(), - {.flags = {{"print_runner_log", "true"}, - {"exit_on_crash", "true"}, - {"workdir", workdir.dirname()}, - {"binary", - absl::StrCat(BinaryPath(kDefaultTargetBinary), " ", - CreateFuzzTestFlag("fuzz", test_name))}, - {"num_runs", absl::StrCat(iterations)}}, - .timeout = absl::InfiniteDuration()}); - } else { - return RunBinary( - BinaryPath(kDefaultTargetBinary), - {.fuzztest_flags = {{"fuzz", std::string(test_name)}}, - .env = {{"FUZZTEST_MAX_FUZZING_RUNS", absl::StrCat(iterations)}}, - .timeout = absl::InfiniteDuration()}); - } - } - - // Counts the number of times the target binary has been run. Needed because - // Centipede runs the binary multiple times. - int CountTargetRuns(absl::string_view std_err) { - if (GetParam().multi_process) { - return CountSubstrs(std_err, "Centipede fuzz target runner; argv[0]:"); - } else { - return 1; + switch (GetParam()) { + case ExecutionModelParam::kSingleBinary: + return RunBinary( + BinaryPath(kDefaultTargetBinary), + {.fuzztest_flags = {{"fuzz", std::string(test_name)}, + {"print_subprocess_log", "true"}}, + .env = {{"FUZZTEST_MAX_FUZZING_RUNS", absl::StrCat(iterations)}}, + .timeout = absl::InfiniteDuration()}); + case ExecutionModelParam::kWithCentipedeBinary: { + TempDir workdir; + return RunBinary( + CentipedePath(), + {.flags = {{"print_runner_log", "true"}, + {"exit_on_crash", "true"}, + {"workdir", workdir.dirname()}, + {"binary", + absl::StrCat(BinaryPath(kDefaultTargetBinary), " ", + CreateFuzzTestFlag("fuzz", test_name))}, + {"num_runs", absl::StrCat(iterations)}}, + .timeout = absl::InfiniteDuration()}); + } + default: + fprintf(stderr, "Unsupported execution model!\n"); + std::abort(); } } }; @@ -1268,6 +1324,7 @@ class FuzzingModeFixtureTest TEST_P(FuzzingModeFixtureTest, GlobalEnvironmentIsSetUpForFailingTest) { auto [status, std_out, std_err] = Run("MySuite.GoogleTestExpect", /*iterations=*/10); + SCOPED_TRACE(TraceMessageForRunResult(status, std_out, std_err)); EXPECT_GT(CountTargetRuns(std_err), 0); EXPECT_EQ( CountTargetRuns(std_err), @@ -1280,6 +1337,7 @@ TEST_P(FuzzingModeFixtureTest, GlobalEnvironmentGoesThroughCompleteLifecycleForSuccessfulTest) { auto [status, std_out, std_err] = Run("MySuite.GoogleTestNeverFails", /*iterations=*/10); + SCOPED_TRACE(TraceMessageForRunResult(status, std_out, std_err)); EXPECT_GT(CountTargetRuns(std_err), 0); EXPECT_EQ( CountTargetRuns(std_err), @@ -1315,13 +1373,16 @@ TEST_P(FuzzingModeFixtureTest, TEST_P(FuzzingModeFixtureTest, GoogleTestPerFuzzTestFixtureInstantiatedOncePerFuzzTest) { auto [status, std_out, std_err] = - Run("CallCountPerFuzzTest.CallCountReachesAtLeastTen", /*iterations=*/10); - EXPECT_THAT(std_err, HasSubstr("<>")); + Run("CallCountPerFuzzTest.CallCountPerFuzzTestEqualsToGlobalCount", + /*iterations=*/10); + SCOPED_TRACE(TraceMessageForRunResult(status, std_out, std_err)); + EXPECT_THAT(status, Eq(ExitCode(0))); } TEST_P(FuzzingModeFixtureTest, GoogleTestStaticTestSuiteFunctionsCalledOnce) { auto [status, std_out, std_err] = - Run("CallCountPerFuzzTest.CallCountReachesAtLeastTen", /*iterations=*/10); + Run("CountPerFuzzTest.CallCountPerFuzzTestEqualsToGlobalCount", + /*iterations=*/10); EXPECT_GT(CountTargetRuns(std_err), 0); EXPECT_EQ(CountTargetRuns(std_err), CountSubstrs(std_err, "<>")); @@ -1383,7 +1444,7 @@ class FuzzingModeCrashFindingTest // There are however env vars that we do want to propagate, which // we now need to do explicitly. env = WithTestSanitizerOptions(std::move(env)); - if (GetParam().multi_process) { + if (GetParam() == ExecutionModelParam::kWithCentipedeBinary) { TempDir workdir; return RunBinary( CentipedePath(), @@ -1405,14 +1466,14 @@ class FuzzingModeCrashFindingTest } void ExpectTargetAbort(TerminationStatus status, absl::string_view std_err) { - if (GetParam().multi_process) { - EXPECT_THAT(status, Ne(ExitCode(0))); - EXPECT_TRUE(RE2::PartialMatch( - std_err, absl::StrCat("Exit code\\s*:\\s*", SIGABRT))) - << std_err; - } else { - EXPECT_THAT(status, Eq(Signal(SIGABRT))); - } +#ifdef FUZZTEST_USE_CENTIPEDE + EXPECT_THAT(status, Ne(ExitCode(0))); + EXPECT_TRUE( + RE2::PartialMatch(std_err, absl::StrCat("Exit code\\s*:\\s*", SIGABRT))) + << std_err; +#else + EXPECT_THAT(status, Eq(Signal(SIGABRT))); +#endif } }; @@ -1796,19 +1857,21 @@ TEST_P(FuzzingModeCrashFindingTest, GTestCrashMetadataIsDumpedIfEnvVarIsSet) { Optional(Eq("GoogleTest assertion failure"))); } -TEST_P(FuzzingModeCrashFindingTest, - CustomMutatorAndMutateCalllbackWorksForLLVMFuzzer) { +INSTANTIATE_TEST_SUITE_P(FuzzingModeCrashFindingTestWithExecutionModel, + FuzzingModeCrashFindingTest, + testing::ValuesIn(GetAvailableExecutionModels())); + +TEST(LLVMFuzzerTest, CustomMutatorAndMutateCalllbackWorks) { TempDir out_dir; auto [status, std_out, std_err] = - Run("LLVMFuzzer.TestOneInput", "testdata/llvm_fuzzer_with_custom_mutator", - /*env=*/{}, /*timeout=*/absl::Seconds(30)); + RunBinary(BinaryPath("testdata/llvm_fuzzer_with_custom_mutator"), + {.flags = {{"logtostderr", "true"}}, + .fuzztest_flags = {{"fuzz", "LLVMFuzzer.TestOneInput"}}, + .timeout = absl::Seconds(30)}); + SCOPED_TRACE(TraceMessageForRunResult(status, std_out, std_err)); EXPECT_THAT(std_err, HasSubstr("argument 0: \"ahmfn\"")); ExpectTargetAbort(status, std_err); } -INSTANTIATE_TEST_SUITE_P(FuzzingModeCrashFindingTestWithExecutionModel, - FuzzingModeCrashFindingTest, - testing::ValuesIn(GetAvailableExecutionModels())); - } // namespace } // namespace fuzztest::internal diff --git a/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc b/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc index 50a1abfb..193bff96 100644 --- a/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc +++ b/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc @@ -65,6 +65,11 @@ using ::fuzztest::internal::TestProtobufWithRequired; using ::fuzztest::internal::TestSubProtobuf; using ::google::protobuf::FieldDescriptor; +bool print_target_run_message_once = []() { + fputs("FuzzTest functional test target run\n", stderr); + return true; +}(); + void PassesWithPositiveInput(int x) { if (x <= 0) std::abort(); } @@ -757,12 +762,18 @@ FUZZ_TEST_F(AlternateSignalStackFixture, void DetectRegressionAndCoverageInputs(const std::string& input) { if (absl::StartsWith(input, "regression")) { - std::cout << "regression input detected: " << input << std::endl; + std::cerr << "regression input detected: " << input << std::endl; } if (absl::StartsWith(input, "coverage")) { - std::cout << "coverage input detected: " << input << std::endl; + std::cerr << "coverage input detected: " << input << std::endl; + // Sleep for the first coverage input for depleteing the + // replay time budget. + static bool first = true; + if (first) { + first = false; + absl::SleepFor(absl::Seconds(2)); + } } - absl::SleepFor(absl::Seconds(0.1)); } FUZZ_TEST(MySuite, DetectRegressionAndCoverageInputs); diff --git a/e2e_tests/testdata/fuzz_tests_using_googletest.cc b/e2e_tests/testdata/fuzz_tests_using_googletest.cc index d2d01caf..5b3dea92 100644 --- a/e2e_tests/testdata/fuzz_tests_using_googletest.cc +++ b/e2e_tests/testdata/fuzz_tests_using_googletest.cc @@ -88,19 +88,18 @@ class CallCountPerIteration FUZZ_TEST_F(CallCountPerIteration, CallCountIsAlwaysIncrementedFromInitialValue); +static int global_call_count = 0; class CallCountPerFuzzTest : public ::fuzztest::PerFuzzTestFixtureAdapter { public: - void CallCountReachesAtLeastTen(int) { - if (call_count_ < std::numeric_limits::max()) ++call_count_; - if (call_count_ == 10) { - fprintf(stderr, "<>\n", - call_count_); - } + void CallCountPerFuzzTestEqualsToGlobalCount(int) { + ++call_count_; + ++global_call_count; + EXPECT_EQ(call_count_, global_call_count); } void NeverFails(int) {} }; -FUZZ_TEST_F(CallCountPerFuzzTest, CallCountReachesAtLeastTen); +FUZZ_TEST_F(CallCountPerFuzzTest, CallCountPerFuzzTestEqualsToGlobalCount); FUZZ_TEST_F(CallCountPerFuzzTest, NeverFails); TEST(SharedSuite, WorksAsUnitTest) {} diff --git a/fuzztest/BUILD b/fuzztest/BUILD index 4dcdb3ce..ce758b54 100644 --- a/fuzztest/BUILD +++ b/fuzztest/BUILD @@ -216,20 +216,26 @@ cc_library( ":corpus_database", ":domain_core", ":fixture_driver", + ":flag_name", + ":io", ":logging", ":runtime", ":table_of_recent_compares", "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/base:no_destructor", + "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/log", "@com_google_absl//absl/memory", "@com_google_absl//absl/random", "@com_google_absl//absl/random:distributions", + "@com_google_absl//absl/status", "@com_google_absl//absl/strings", "@com_google_absl//absl/strings:str_format", "@com_google_absl//absl/strings:string_view", "@com_google_absl//absl/time", "@com_google_absl//absl/types:span", "@com_google_fuzztest//centipede:centipede_callbacks", + "@com_google_fuzztest//centipede:centipede_default_callbacks", "@com_google_fuzztest//centipede:centipede_interface", "@com_google_fuzztest//centipede:centipede_runner_no_main", "@com_google_fuzztest//centipede:environment", diff --git a/fuzztest/init_fuzztest.cc b/fuzztest/init_fuzztest.cc index 2c113189..9836307c 100644 --- a/fuzztest/init_fuzztest.cc +++ b/fuzztest/init_fuzztest.cc @@ -163,6 +163,16 @@ FUZZTEST_DEFINE_FLAG(std::optional, jobs, std::nullopt, "The number of fuzzing jobs to run in parallel. If " "unspecified, the number of jobs is 1."); +FUZZTEST_DEFINE_FLAG(bool, fuzz_in_unit_test_mode, false, + "If set, force to use unit test mode in fuzzing."); + +FUZZTEST_DEFINE_FLAG(bool, print_subprocess_log, false, + "If set, always print stdout/stderr of the subprocesses. " + "This is mainly used for tool testing/debugging."); + +FUZZTEST_DEFINE_FLAG(std::optional, + internal_crashing_input_to_reproduce, std::nullopt, ""); + namespace fuzztest { std::vector ListRegisteredTests() { @@ -213,9 +223,6 @@ namespace { std::optional GetFuzzingTime() { absl::Duration fuzz_time_limit = absl::GetFlag(FUZZTEST_FLAG(fuzz_for)); - if (fuzz_time_limit <= absl::ZeroDuration()) { - fuzz_time_limit = absl::InfiniteDuration(); - } if (absl::GetFlag(FUZZTEST_FLAG(fuzz)) == kUnspecified && fuzz_time_limit == absl::InfiniteDuration()) { return std::nullopt; @@ -257,11 +264,22 @@ internal::Configuration CreateConfigurationsFromFlags( std::string(binary_identifier), /*fuzz_tests=*/ListRegisteredTests(), /*fuzz_tests_in_current_shard=*/ListRegisteredTests(), + /*replay_corpus=*/ + (fuzzing_time_limit.has_value() && + !absl::GetFlag(FUZZTEST_FLAG(fuzz_in_unit_test_mode))) || + replay_corpus_time_limit.has_value(), + /*replay_only=*/ + (fuzzing_time_limit.has_value() && + *fuzzing_time_limit == absl::ZeroDuration()) || + replay_corpus_time_limit.has_value(), reproduce_findings_as_separate_tests, + /*replay_in_single_process=*/false, + absl::GetFlag(FUZZTEST_FLAG(print_subprocess_log)), /*stack_limit=*/absl::GetFlag(FUZZTEST_FLAG(stack_limit_kb)) * 1024, /*rss_limit=*/absl::GetFlag(FUZZTEST_FLAG(rss_limit_mb)) * 1024 * 1024, absl::GetFlag(FUZZTEST_FLAG(time_limit_per_input)), time_limit, - absl::GetFlag(FUZZTEST_FLAG(time_budget_type)), jobs.value_or(0)}; + absl::GetFlag(FUZZTEST_FLAG(time_budget_type)), jobs.value_or(0), + absl::GetFlag(FUZZTEST_FLAG(internal_crashing_input_to_reproduce))}; } } // namespace @@ -303,6 +321,9 @@ void InitFuzzTest(int* argc, char*** argv, std::string_view binary_id) { GetMatchingFuzzTestOrExit(specified_test); // Delegate the test to GoogleTest. GTEST_FLAG_SET(filter, matching_fuzz_test); + // Do not list tests when the fuzz test is specified. Needed for + // multi-process mode to work where args are passed through. + GTEST_FLAG_SET(list_tests, false); } std::string derived_binary_id = @@ -342,7 +363,10 @@ void InitFuzzTest(int* argc, char*** argv, std::string_view binary_id) { } } const RunMode run_mode = - fuzzing_time_limit.has_value() ? RunMode::kFuzz : RunMode::kUnitTest; + (!absl::GetFlag(FUZZTEST_FLAG(fuzz_in_unit_test_mode)) && + fuzzing_time_limit.has_value()) + ? RunMode::kFuzz + : RunMode::kUnitTest; // TODO(b/307513669): Use the Configuration class instead of Runtime. internal::Runtime::instance().SetRunMode(run_mode); } diff --git a/fuzztest/internal/centipede_adaptor.cc b/fuzztest/internal/centipede_adaptor.cc index 292e450c..61295b93 100644 --- a/fuzztest/internal/centipede_adaptor.cc +++ b/fuzztest/internal/centipede_adaptor.cc @@ -15,6 +15,13 @@ #include "./fuzztest/internal/centipede_adaptor.h" #include +#ifdef __APPLE__ +#include +#else // __APPLE__ +#include // ARG_MAX +#endif // __APPLE__ +#include +#include #include #include @@ -37,19 +44,25 @@ #include #include "absl/algorithm/container.h" +#include "absl/base/no_destructor.h" +#include "absl/container/flat_hash_map.h" #include "absl/log/log.h" #include "absl/memory/memory.h" #include "absl/random/distributions.h" #include "absl/random/random.h" +#include "absl/status/status.h" #include "absl/strings/match.h" #include "absl/strings/numbers.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/string_view.h" #include "absl/time/clock.h" #include "absl/time/time.h" #include "absl/types/span.h" #include "./centipede/centipede_callbacks.h" +#include "./centipede/centipede_default_callbacks.h" #include "./centipede/centipede_interface.h" #include "./centipede/environment.h" #include "./centipede/mutation_input.h" @@ -64,6 +77,8 @@ #include "./fuzztest/internal/corpus_database.h" #include "./fuzztest/internal/domains/domain.h" #include "./fuzztest/internal/fixture_driver.h" +#include "./fuzztest/internal/flag_name.h" +#include "./fuzztest/internal/io.h" #include "./fuzztest/internal/logging.h" #include "./fuzztest/internal/runtime.h" #include "./fuzztest/internal/table_of_recent_compares.h" @@ -97,6 +112,66 @@ class TempDir { std::string path_; }; +// TODO(xinhaoyuan): Consolidate the similar code in +// centipede/runner_fork_server.cc +absl::StatusOr> GetArgs() { + std::vector result; +#ifdef __APPLE__ + char args[ARG_MAX]; + size_t args_size = sizeof(args); + int mib[] = {CTL_KERN, KERN_PROCARGS2, getpid()}; + int rv = + sysctl(mib, sizeof(mib) / sizeof(mib[0]), args, &args_size, nullptr, 0); + if (rv != 0) { + return absl::InternalError( + "GetEnv: sysctl({CTK_KERN, KERN_PROCARGS2, ...}) failed"); + } + if (args_size < sizeof(int)) { + return absl::InternalError("GetEnv: args_size too small"); + } + int argc = 0; + memcpy(&argc, &args[0], sizeof(argc)); + size_t start_pos = sizeof(argc); + // Find the end of the executable path. + while (start_pos < args_size && args[start_pos] != 0) ++start_pos; + if (start_pos == args_size) { + return absl::NotFoundError("nothing after executable path"); + } + // Find the beginning of the string area. + while (start_pos < args_size && args[start_pos] == 0) ++start_pos; + if (start_pos == args_size) { + return absl::NotFoundError("nothing after executable path"); + } + // Get the first argc c-strings without exceeding the boundary. + for (int i = 0; i < argc; ++i) { + const size_t current_argv_pos = start_pos; + while (start_pos < args_size && args[start_pos] != 0) ++start_pos; + result.emplace_back(&args[current_argv_pos], start_pos - current_argv_pos); + ++start_pos; + } +#else + const int fd = open("/proc/self/cmdline", O_RDONLY); + if (fd < 0) return absl::InternalError("cannot open /proc/self/cmdline\n"); + char args[4096]; + const ssize_t args_size = read(fd, args, sizeof(args)); + if (args_size < 0) + return absl::InternalError("cannot open /proc/self/cmdline\n"); + if (close(fd) != 0) LOG(ERROR) << "cannot close /proc/self/cmdline\n"; + size_t start_pos = 0; + while (start_pos < args_size) { + const size_t current_argv_pos = start_pos; + while (start_pos < args_size && args[start_pos] != 0) ++start_pos; + result.emplace_back(&args[current_argv_pos], start_pos - current_argv_pos); + ++start_pos; + } +#endif + return result; +} + +std::string ShellEscape(absl::string_view str) { + return absl::StrCat("'", absl::StrReplaceAll(str, {{"'", "'\\''"}}), "'"); +} + // TODO(xinhaoyuan): Consider passing rng seeds from the engine. std::seed_seq GetRandomSeed() { const size_t seed = time(nullptr) + getpid() + @@ -119,26 +194,126 @@ centipede::Environment CreateDefaultCentipedeEnvironment() { centipede::Environment CreateCentipedeEnvironmentFromFuzzTestFlags( const Configuration& configuration, absl::string_view workdir, - absl::string_view test_name) { + absl::string_view test_name, RunMode run_mode) { centipede::Environment env = CreateDefaultCentipedeEnvironment(); - env.workdir = workdir; - env.exit_on_crash = true; - // Populating the PC table in single-process mode is not implemented. + constexpr absl::Duration kUnitTestDuration = absl::Seconds(10); + env.populate_binary_info = false; +#ifndef FUZZTEST_USE_CENTIPEDE_SINGLE_PROCESS + if (configuration.jobs != 0) { + env.num_threads = configuration.jobs; + env.total_shards = configuration.jobs; + env.my_shard_index = 0; + } + const auto args = GetArgs(); + FUZZTEST_INTERNAL_CHECK(args.ok(), + "Failed to get the original process args\n"); + env.binary.clear(); + for (const auto& arg : *args) { + if (absl::StartsWith(arg, "--" FUZZTEST_FLAG_PREFIX "fuzz=")) continue; + if (absl::StartsWith(arg, "--" FUZZTEST_FLAG_PREFIX "fuzz_for=")) continue; + if (absl::StartsWith(arg, "--" FUZZTEST_FLAG_PREFIX "replay_corpus=")) + continue; + if (absl::StartsWith(arg, "--" FUZZTEST_FLAG_PREFIX "replay_corpus_for=")) + continue; + // We need shell escaping, because parts of binary_arg will be passed to + // system(), which uses the default shell. + absl::StrAppend(&env.binary, env.binary.empty() ? "" : " ", + ShellEscape(arg)); + } + if (configuration.crashing_input_to_reproduce.has_value()) { + absl::StrAppend(&env.binary, " --" FUZZTEST_FLAG_PREFIX "fuzz=", test_name); + absl::StrAppend(&env.binary, + " --" FUZZTEST_FLAG_PREFIX + "internal_crashing_input_to_reproduce=", + *configuration.crashing_input_to_reproduce); + env.run_crash_id = *configuration.crashing_input_to_reproduce; + env.load_shards_only = true; + run_mode = RunMode::kFuzz; + } + // The logic is very hairy - see `CreateConfigurationsFromFlags`. + // + // replay corpus iff: + // running in fuzzing mode, or --replay_corpus/replay_corpus_for is set. + // + // repaly without fuzzing iff: + // --replay_corpus/replay_corpus_for is set, or fuzzing the duration is 0. + else if (configuration.replay_only) { + env.load_shards_only = true; + if (configuration.replay_corpus) { + absl::StrAppend(&env.binary, + " --" FUZZTEST_FLAG_PREFIX "replay_corpus=", test_name); + absl::StrAppend(&env.binary, + " --" FUZZTEST_FLAG_PREFIX "replay_corpus_for=", + configuration.GetTimeLimitPerTest()); + } else { + absl::StrAppend(&env.binary, + " --" FUZZTEST_FLAG_PREFIX "fuzz=", test_name); + absl::StrAppend(&env.binary, " --" FUZZTEST_FLAG_PREFIX "fuzz_for=0"); + } + } else { + FUZZTEST_INTERNAL_CHECK( + (run_mode == RunMode::kFuzz) == configuration.replay_corpus, + "should set to replay corpus if and only if in fuzzing mode"); + absl::StrAppend(&env.binary, " --" FUZZTEST_FLAG_PREFIX "fuzz=", test_name); + absl::StrAppend(&env.binary, " --" FUZZTEST_FLAG_PREFIX "fuzz_for=", + (run_mode == RunMode::kFuzz) + ? configuration.GetTimeLimitPerTest() + : kUnitTestDuration); + } + if (run_mode == RunMode::kUnitTest) { + absl::StrAppend(&env.binary, + " --" FUZZTEST_FLAG_PREFIX "fuzz_in_unit_test_mode"); + } + env.coverage_binary = (*args)[0]; + env.exit_on_crash = run_mode == RunMode::kUnitTest || + configuration.corpus_database.empty() || + configuration.crashing_input_to_reproduce.has_value(); + env.print_runner_log = configuration.print_subprocess_log; +#else env.require_pc_table = false; - const auto time_limit_per_test = configuration.GetTimeLimitPerTest(); - if (time_limit_per_test != absl::InfiniteDuration()) { - absl::FPrintF(GetStderr(), "[.] Fuzzing timeout set to: %s\n", - absl::FormatDuration(time_limit_per_test)); - env.stop_at = absl::Now() + time_limit_per_test; - } - env.first_corpus_dir_output_only = true; - if (const char* corpus_out_dir_chars = getenv("FUZZTEST_TESTSUITE_OUT_DIR")) { - env.corpus_dir.push_back(corpus_out_dir_chars); + env.exit_on_crash = true; +#endif + env.workdir = workdir; + if (configuration.corpus_database.empty()) { + const auto time_limit_per_test = (run_mode == RunMode::kFuzz) + ? configuration.GetTimeLimitPerTest() + : kUnitTestDuration; + if (time_limit_per_test != absl::InfiniteDuration()) { + absl::FPrintF(GetStderr(), "[.] Fuzzing timeout set to: %s\n", + absl::FormatDuration(time_limit_per_test)); + env.stop_at = absl::Now() + time_limit_per_test; + } + env.first_corpus_dir_output_only = true; + if (const char* corpus_out_dir_chars = + getenv("FUZZTEST_TESTSUITE_OUT_DIR")) { + env.corpus_dir.push_back(corpus_out_dir_chars); + } else { + env.corpus_dir.push_back(""); + } + if (const char* corpus_in_dir_chars = getenv("FUZZTEST_TESTSUITE_IN_DIR")) { + env.corpus_dir.push_back(corpus_in_dir_chars); + } } else { - env.corpus_dir.push_back(""); + // Not setting env.stop_at since current update_corpus logic in Centipede + // would propagate that. + if (getenv("FUZZTEST_TESTSUITE_OUT_DIR")) { + absl::FPrintF(GetStderr(), + "[!] Ignoring FUZZTEST_TESTSUITE_OUT_DIR when the corpus " + "database is set.\n"); + } + if (getenv("FUZZTEST_TESTSUITE_IN_DIR")) { + absl::FPrintF(GetStderr(), + "[!] Ignoring FUZZTEST_TESTSUITE_IN_DIR when the corpus " + "database is set.\n"); + } + if (getenv("FUZZTEST_MINIMIZE_TESTSUITE_DIR")) { + absl::FPrintF(GetStderr(), + "[!] Ignoring FUZZTEST_MINIMIZE_TESTSUITE_DIR when the " + "corpus database is set.\n"); + } + // For now Centipede will generate workdir/corpus dirs from the test + // configuration of the FuzzTest sub-processes. So no need to set them here. } - if (const char* corpus_in_dir_chars = getenv("FUZZTEST_TESTSUITE_IN_DIR")) - env.corpus_dir.push_back(corpus_in_dir_chars); if (const char* max_fuzzing_runs = getenv("FUZZTEST_MAX_FUZZING_RUNS")) { if (!absl::SimpleAtoi(max_fuzzing_runs, &env.num_runs)) { absl::FPrintF(GetStderr(), @@ -148,11 +323,69 @@ centipede::Environment CreateCentipedeEnvironmentFromFuzzTestFlags( env.num_runs = std::numeric_limits::max(); } } + LOG(INFO) << "Created env with binary=" << env.binary + << ", workdir=" << env.workdir + << ", replay_corpus=" << configuration.replay_corpus + << ", replay_only=" << configuration.replay_only; return env; } } // namespace +void OnRuntimeTerminationRequested() { + if (!centipede::ShouldStop()) centipede::RequestEarlyStop(0); +} + +std::vector ListCrashInputs(const Configuration& configuration, + absl::string_view test_name) { + // Do not invoke Centipede to list crashes as a runner, which is also + // unnecesary. + if (getenv("CENTIPEDE_RUNNER_FLAGS")) return {}; + static absl::NoDestructor< + absl::flat_hash_map>> + crash_inputs_for_test([&configuration, test_name] { + std::vector results; + TempDir workspace("/tmp/fuzztest-"); + auto env = CreateCentipedeEnvironmentFromFuzzTestFlags( + configuration, /*workdir=*/"", test_name, + Runtime::instance().run_mode()); + env.list_crash_ids_to_file = + std::filesystem::path{workspace.path()} / "crash_ids"; + + centipede::DefaultCallbacksFactory + callbacks; + const int centipede_ret = centipede::CentipedeMain(env, callbacks); + absl::flat_hash_map> + crash_inputs_for_test; + if (centipede_ret != EXIT_SUCCESS) { + LOG(WARNING) << "Cannot list crash IDs using Centipede - returning " + "empty results."; + return crash_inputs_for_test; + } + const auto contents = ReadFile(env.list_crash_ids_to_file); + if (!contents.has_value()) { + LOG(WARNING) << "Cannot read the result file from listing crash IDs " + "with Centipede - returning empty results."; + return crash_inputs_for_test; + } + for (absl::string_view line : absl::StrSplit(*contents, '\n')) { + const auto delim_pos = line.find('/'); + if (delim_pos == line.npos) { + LOG(WARNING) << "Cannot parse the line in the result file of crash " + "IDs from Centipede: " + << line; + continue; + } + const auto test_name = line.substr(0, delim_pos); + const auto crash_id = std::string{line.substr(delim_pos + 1)}; + crash_inputs_for_test[test_name].push_back(crash_id); + } + return crash_inputs_for_test; + }()); + if (!crash_inputs_for_test->contains(test_name)) return {}; + return (*crash_inputs_for_test)[test_name]; +} + class CentipedeAdaptorRunnerCallbacks : public centipede::RunnerCallbacks { public: CentipedeAdaptorRunnerCallbacks(Runtime* runtime, @@ -161,8 +394,8 @@ class CentipedeAdaptorRunnerCallbacks : public centipede::RunnerCallbacks { : runtime_(*runtime), fuzzer_impl_(*fuzzer_impl), configuration_(*configuration), - prng_(GetRandomSeed()) { - } + cmp_tables_(std::make_unique()), + prng_(GetRandomSeed()) {} bool Execute(centipede::ByteSpan input) override { auto parsed_input = @@ -178,13 +411,13 @@ class CentipedeAdaptorRunnerCallbacks : public centipede::RunnerCallbacks { std::function seed_callback) override { std::vector seeds = fuzzer_impl_.fixture_driver_->GetSeeds(); - CorpusDatabase corpus_database(configuration_); - fuzzer_impl_.ForEachInput( - corpus_database.GetCoverageInputsIfAny(fuzzer_impl_.test_.full_name()), - [&](absl::string_view /*file_path*/, std::optional /*blob_idx*/, - FuzzTestFuzzerImpl::Input input) { - seeds.push_back(std::move(input.args)); - }); + // CorpusDatabase corpus_database(configuration_); + // fuzzer_impl_.ForEachInput( + // corpus_database.GetCoverageInputsIfAny(fuzzer_impl_.test_.full_name()), + // [&](absl::string_view /*file_path*/, std::optional /*blob_idx*/, + // FuzzTestFuzzerImpl::Input input) { + // seeds.push_back(std::move(input.args)); + // }); constexpr int kInitialValuesInSeeds = 32; for (int i = 0; i < kInitialValuesInSeeds; ++i) { seeds.push_back(fuzzer_impl_.params_domain_.Init(prng_)); @@ -229,7 +462,7 @@ class CentipedeAdaptorRunnerCallbacks : public centipede::RunnerCallbacks { size_t num_mutants, std::function new_mutant_callback) override { if (inputs.empty()) return false; - SetMetadata(inputs[0].metadata); + if (runtime_.run_mode() == RunMode::kFuzz) SetMetadata(inputs[0].metadata); for (size_t i = 0; i < num_mutants; ++i) { const auto choice = absl::Uniform(prng_, 0, 1); std::string mutant_data; @@ -248,7 +481,8 @@ class CentipedeAdaptorRunnerCallbacks : public centipede::RunnerCallbacks { parsed_origin = fuzzer_impl_.params_domain_.Init(prng_); } auto mutant = FuzzTestFuzzerImpl::Input{*std::move(parsed_origin)}; - fuzzer_impl_.MutateValue(mutant, prng_, {.cmp_tables = &cmp_tables_}); + fuzzer_impl_.MutateValue(mutant, prng_, + {.cmp_tables = cmp_tables_.get()}); mutant_data = fuzzer_impl_.params_domain_.SerializeCorpus(mutant.args).ToString(); } @@ -269,7 +503,7 @@ class CentipedeAdaptorRunnerCallbacks : public centipede::RunnerCallbacks { T b_int; memcpy(&a_int, a, sizeof(T)); memcpy(&b_int, b, sizeof(T)); - cmp_tables_.GetMutable().Insert(a_int, b_int); + cmp_tables_->GetMutable().Insert(a_int, b_int); } void SetMetadata(const centipede::ExecutionMetadata* metadata) { @@ -288,7 +522,7 @@ class CentipedeAdaptorRunnerCallbacks : public centipede::RunnerCallbacks { } else if (size == 8) { InsertCmpEntryIntoIntegerDictionary(a.data(), b.data()); } - cmp_tables_.GetMutable<0>().Insert(a.data(), b.data(), size); + cmp_tables_->GetMutable<0>().Insert(a.data(), b.data(), size); }); } @@ -299,7 +533,7 @@ class CentipedeAdaptorRunnerCallbacks : public centipede::RunnerCallbacks { Runtime& runtime_; FuzzTestFuzzerImpl& fuzzer_impl_; const Configuration& configuration_; - TablesOfRecentCompares cmp_tables_; + std::unique_ptr cmp_tables_; absl::BitGen prng_; }; @@ -352,10 +586,6 @@ class CentipedeAdaptorEngineCallbacks : public centipede::CentipedeCallbacks { buffer_offset); batch_result.Read(batch_result_blobseq); } - if (runtime_.termination_requested() && !centipede::ShouldStop()) { - absl::FPrintF(GetStderr(), "[.] Early termination requested.\n"); - centipede::RequestEarlyStop(0); - } return true; } @@ -479,7 +709,7 @@ class CentipedeFixtureDriver : public UntypedFixtureDriver { if (runtime_.skipping_requested()) { CentipedeSetExecutionResult(nullptr, 0); } - if (!runner_mode) CentipedeFinalizeProcessing(); + CentipedeFinalizeProcessing(); } void TearDownFuzzTest() override { orig_fixture_driver_->TearDownFuzzTest(); } @@ -517,32 +747,50 @@ CentipedeFuzzerAdaptor::CentipedeFuzzerAdaptor( "Invalid fixture driver!"); } -void CentipedeFuzzerAdaptor::RunInUnitTestMode( +bool CentipedeFuzzerAdaptor::RunInUnitTestMode( const Configuration& configuration) { - centipede_fixture_driver_->set_configuration(&configuration); - CentipedeBeginExecutionBatch(); - fuzzer_impl_.RunInUnitTestMode(configuration); - CentipedeEndExecutionBatch(); + return Run(/*argc=*/nullptr, /*argv=*/nullptr, RunMode::kUnitTest, + configuration) == 0; } int CentipedeFuzzerAdaptor::RunInFuzzingMode( int* argc, char*** argv, const Configuration& configuration) { + return Run(argc, argv, RunMode::kFuzz, configuration); +} + +int CentipedeFuzzerAdaptor::Run(int* argc, char*** argv, RunMode mode, + const Configuration& configuration) { centipede_fixture_driver_->set_configuration(&configuration); - runtime_.SetRunMode(RunMode::kFuzz); - runtime_.SetSkippingRequested(false); - runtime_.SetCurrentTest(&test_, &configuration); - if (IsSilenceTargetEnabled()) SilenceTargetStdoutAndStderr(); - runtime_.EnableReporter(&fuzzer_impl_.stats_, [] { return absl::Now(); }); - fuzzer_impl_.fixture_driver_->SetUpFuzzTest(); - // Always create a new domain input to trigger any domain setup - // failures here. (e.g. Ineffective Filter) - FuzzTestFuzzerImpl::PRNG prng; - fuzzer_impl_.params_domain_.Init(prng); - bool print_final_stats = true; // When the CENTIPEDE_RUNNER_FLAGS env var exists, the current process is // considered a child process spawned by the Centipede binary as the runner, // and we should not run CentipedeMain in this process. const bool runner_mode = getenv("CENTIPEDE_RUNNER_FLAGS"); +#ifndef FUZZTEST_USE_CENTIPEDE_SINGLE_PROCESS + const bool is_running_property_function_in_this_process = + runner_mode || + (configuration.crashing_input_to_reproduce.has_value() && + configuration.replay_in_single_process) || + getenv("FUZZTEST_REPLAY") || getenv("FUZZTEST_MINIMIZE_REPRODUCER"); +#else // FUZZTEST_USE_CENTIPEDE_SINGLE_PROCESS + const bool is_running_property_function_in_this_process = true; +#endif // FUZZTEST_USE_CENTIPEDE_SINGLE_PROCESS + runtime_.SetRunMode(mode); + runtime_.SetSkippingRequested(false); + runtime_.SetCurrentTest(&test_, &configuration); + if (is_running_property_function_in_this_process) { + if (IsSilenceTargetEnabled()) SilenceTargetStdoutAndStderr(); + runtime_.EnableReporter(&fuzzer_impl_.stats_, [] { return absl::Now(); }); + } else { + InstallSignalHandlers(GetStderr(), /*install_crash_handlers=*/false); + } + fuzzer_impl_.fixture_driver_->SetUpFuzzTest(); + if (runner_mode) { + // Always create a new domain input to trigger any domain setup + // failures here. (e.g. Ineffective Filter) + FuzzTestFuzzerImpl::PRNG prng; + fuzzer_impl_.params_domain_.Init(prng); + } + bool print_final_stats = true; const int result = ([&]() { if (runtime_.skipping_requested()) { absl::FPrintF(GetStderr(), @@ -554,22 +802,62 @@ int CentipedeFuzzerAdaptor::RunInFuzzingMode( CentipedeAdaptorRunnerCallbacks runner_callbacks(&runtime_, &fuzzer_impl_, &configuration); print_final_stats = false; - return centipede::RunnerMain(argc != nullptr ? *argc : 0, - argv != nullptr ? *argv : nullptr, + static char fake_argv0[] = "fake_argv"; + static char* fake_argv[] = {fake_argv0, nullptr}; + return centipede::RunnerMain(argc != nullptr ? *argc : 1, + argv != nullptr ? *argv : fake_argv, runner_callbacks); } // Centipede engine does not support replay and reproducer minimization // (within the single process). So use the existing fuzztest implementation. // This is fine because it does not require coverage instrumentation. - if (fuzzer_impl_.ReplayInputsIfAvailable(configuration)) return 0; + if (!configuration.crashing_input_to_reproduce.has_value() && + fuzzer_impl_.ReplayInputsIfAvailable(configuration)) { + return 0; + } + // `ReplayInputsIfAvailable` overwrites the run mode - revert it back. + runtime_.SetRunMode(mode); // Run as the fuzzing engine. - TempDir workdir("/tmp/fuzztest-workdir-"); + std::unique_ptr workdir; + if (configuration.corpus_database.empty() || mode == RunMode::kUnitTest) + workdir = std::make_unique("/tmp/fuzztest-workdir-"); + const std::string workdir_path = workdir ? workdir->path() : ""; const auto env = CreateCentipedeEnvironmentFromFuzzTestFlags( - configuration, workdir.path(), test_.full_name()); + configuration, workdir_path, test_.full_name(), mode); +#ifndef FUZZTEST_USE_CENTIPEDE_SINGLE_PROCESS + // No stats will be available. + print_final_stats = false; + centipede::DefaultCallbacksFactory + factory; +#else // FUZZTEST_USE_CENTIPEDE_SINGLE_PROCESS CentipedeAdaptorEngineCallbacksFactory factory(&runtime_, &fuzzer_impl_, &configuration); - if (const char* minimize_dir_chars = - std::getenv("FUZZTEST_MINIMIZE_TESTSUITE_DIR")) { +#endif // FUZZTEST_USE_CENTIPEDE_SINGLE_PROCESS + if (!configuration.corpus_database.empty()) { + if (configuration.replay_in_single_process && + configuration.crashing_input_to_reproduce.has_value()) { + TempDir crash_fetch_dir("/tmp/fuzztest-"); + auto fetch_crash_env = env; + std::string crash_file = + (std::filesystem::path(crash_fetch_dir.path()) / "crash").string(); + fetch_crash_env.fetch_crash_to_file = crash_file; + if (centipede::CentipedeMain(fetch_crash_env, factory) != + EXIT_SUCCESS) { + absl::FPrintF( + GetStderr(), + "[!] Encountered error when using Centipede to fetch the crash " + "input. Skipping the test to avoid confusing with a real crash"); + return 0; + } + CentipedeAdaptorRunnerCallbacks runner_callbacks( + &runtime_, &fuzzer_impl_, &configuration); + print_final_stats = false; + static char replay_argv0[] = "replay_argv"; + char* replay_argv[] = {replay_argv0, crash_file.data()}; + return centipede::RunnerMain(2, replay_argv, runner_callbacks); + } + } else if (const char* minimize_dir_chars = + std::getenv("FUZZTEST_MINIMIZE_TESTSUITE_DIR")) { print_final_stats = false; const std::string minimize_dir = minimize_dir_chars; const char* corpus_out_dir_chars = @@ -619,16 +907,16 @@ int CentipedeFuzzerAdaptor::RunInFuzzingMode( corpus_out_dir); return 0; } + absl::FPrintF(GetStderr(), "[.] Calling CentipedeMain\n"); return centipede::CentipedeMain(env, factory); })(); fuzzer_impl_.fixture_driver_->TearDownFuzzTest(); - if (result) std::exit(result); - if (print_final_stats) { + if (is_running_property_function_in_this_process && print_final_stats) { absl::FPrintF(GetStderr(), "\n[.] Fuzzing was terminated.\n"); runtime_.PrintFinalStatsOnDefaultSink(); absl::FPrintF(GetStderr(), "\n"); } - return 0; + return result; } } // namespace fuzztest::internal @@ -669,6 +957,7 @@ extern "C" const char* CentipedeGetRunnerFlags() { // Runner mode. Use the existing flags. return strdup(runner_flags_env); } + // Set the runner flags according to the FuzzTest default environment. const auto env = fuzztest::internal::CreateDefaultCentipedeEnvironment(); CentipedeCallbacksForRunnerFlagsExtraction callbacks(env); diff --git a/fuzztest/internal/centipede_adaptor.h b/fuzztest/internal/centipede_adaptor.h index f64bd652..62912731 100644 --- a/fuzztest/internal/centipede_adaptor.h +++ b/fuzztest/internal/centipede_adaptor.h @@ -30,11 +30,14 @@ class CentipedeFuzzerAdaptor : public FuzzTestFuzzer { public: CentipedeFuzzerAdaptor(const FuzzTest& test, std::unique_ptr fixture_driver); - void RunInUnitTestMode(const Configuration& configuration) override; + bool RunInUnitTestMode(const Configuration& configuration) override; int RunInFuzzingMode(int* argc, char*** argv, const Configuration& configuration) override; private: + int Run(int* argc, char*** argv, RunMode mode, + const Configuration& configuration); + Runtime& runtime_ = Runtime::instance(); const FuzzTest& test_; CentipedeFixtureDriver* centipede_fixture_driver_; diff --git a/fuzztest/internal/configuration.cc b/fuzztest/internal/configuration.cc index 865679ba..38746770 100644 --- a/fuzztest/internal/configuration.cc +++ b/fuzztest/internal/configuration.cc @@ -204,12 +204,14 @@ std::string Configuration::Serialize() const { std::string out; out.resize(SpaceFor(corpus_database) + SpaceFor(stats_root) + SpaceFor(binary_identifier) + SpaceFor(fuzz_tests) + - SpaceFor(fuzz_tests_in_current_shard) + + SpaceFor(fuzz_tests_in_current_shard) + SpaceFor(replay_corpus) + + SpaceFor(replay_only) + SpaceFor(reproduce_findings_as_separate_tests) + - SpaceFor(stack_limit) + SpaceFor(rss_limit) + - SpaceFor(time_limit_per_input_str) + SpaceFor(time_limit_str) + - SpaceFor(time_budget_type_str) + SpaceFor(jobs) + - SpaceFor(crashing_input_to_reproduce) + + SpaceFor(replay_in_single_process) + + SpaceFor(print_subprocess_log) + SpaceFor(stack_limit) + + SpaceFor(rss_limit) + SpaceFor(time_limit_per_input_str) + + SpaceFor(time_limit_str) + SpaceFor(time_budget_type_str) + + SpaceFor(jobs) + SpaceFor(crashing_input_to_reproduce) + SpaceFor(reproduction_command_template)); size_t offset = 0; offset = WriteString(out, offset, corpus_database); @@ -217,7 +219,11 @@ std::string Configuration::Serialize() const { offset = WriteString(out, offset, binary_identifier); offset = WriteVectorOfStrings(out, offset, fuzz_tests); offset = WriteVectorOfStrings(out, offset, fuzz_tests_in_current_shard); + offset = WriteIntegral(out, offset, replay_corpus); + offset = WriteIntegral(out, offset, replay_only); offset = WriteIntegral(out, offset, reproduce_findings_as_separate_tests); + offset = WriteIntegral(out, offset, replay_in_single_process); + offset = WriteIntegral(out, offset, print_subprocess_log); offset = WriteIntegral(out, offset, stack_limit); offset = WriteIntegral(out, offset, rss_limit); offset = WriteString(out, offset, time_limit_per_input_str); @@ -239,8 +245,12 @@ absl::StatusOr Configuration::Deserialize( ASSIGN_OR_RETURN(fuzz_tests, ConsumeVectorOfStrings(serialized)); ASSIGN_OR_RETURN(fuzz_tests_in_current_shard, ConsumeVectorOfStrings(serialized)); + ASSIGN_OR_RETURN(replay_corpus, Consume(serialized)); + ASSIGN_OR_RETURN(replay_only, Consume(serialized)); ASSIGN_OR_RETURN(reproduce_findings_as_separate_tests, Consume(serialized)); + ASSIGN_OR_RETURN(replay_in_single_process, Consume(serialized)); + ASSIGN_OR_RETURN(print_subprocess_log, Consume(serialized)); ASSIGN_OR_RETURN(stack_limit, Consume(serialized)); ASSIGN_OR_RETURN(rss_limit, Consume(serialized)); ASSIGN_OR_RETURN(time_limit_per_input_str, ConsumeString(serialized)); @@ -265,7 +275,11 @@ absl::StatusOr Configuration::Deserialize( *std::move(binary_identifier), *std::move(fuzz_tests), *std::move(fuzz_tests_in_current_shard), + *replay_corpus, + *replay_only, *reproduce_findings_as_separate_tests, + *replay_in_single_process, + *print_subprocess_log, *stack_limit, *rss_limit, *time_limit_per_input, diff --git a/fuzztest/internal/configuration.h b/fuzztest/internal/configuration.h index 99d15d94..9ae6d839 100644 --- a/fuzztest/internal/configuration.h +++ b/fuzztest/internal/configuration.h @@ -56,9 +56,14 @@ struct Configuration { std::vector fuzz_tests; // The fuzz tests in the current shard. std::vector fuzz_tests_in_current_shard; + bool replay_corpus = true; + bool replay_only = false; // Generate separate TESTs that replay crashing inputs for the selected fuzz // tests. bool reproduce_findings_as_separate_tests = false; + bool replay_in_single_process = false; + + bool print_subprocess_log = false; // Stack limit in bytes. size_t stack_limit = 128 * 1024; diff --git a/fuzztest/internal/configuration_test.cc b/fuzztest/internal/configuration_test.cc index 83a2cb20..46f94ba6 100644 --- a/fuzztest/internal/configuration_test.cc +++ b/fuzztest/internal/configuration_test.cc @@ -19,8 +19,12 @@ MATCHER_P(IsOkAndEquals, config, "") { config.fuzz_tests == other->fuzz_tests && config.fuzz_tests_in_current_shard == other->fuzz_tests_in_current_shard && + config.replay_corpus == other->replay_corpus && + config.replay_only == other->replay_only && config.reproduce_findings_as_separate_tests == other->reproduce_findings_as_separate_tests && + config.replay_in_single_process == other->replay_in_single_process && + config.print_subprocess_log == other->print_subprocess_log && config.stack_limit == other->stack_limit && config.rss_limit == other->rss_limit && config.time_limit_per_input == other->time_limit_per_input && @@ -40,7 +44,11 @@ TEST(ConfigurationTest, "binary_identifier", /*fuzz_tests=*/{}, /*fuzz_tests_in_current_shard=*/{}, + /*replay_corpus=*/true, + /*replay_only=*/true, /*reproduce_findings_as_separate_tests=*/true, + /*replay_in_single_process=*/true, + /*print_subprocess_log*/ true, /*stack_limit=*/100, /*rss_limit=*/200, /*time_limit_per_input=*/absl::Seconds(42), @@ -61,7 +69,11 @@ TEST(ConfigurationTest, "binary_identifier", {"FuzzTest1", "FuzzTest2"}, {"FuzzTest1"}, + /*replay_corpus=*/true, + /*replay_only=*/true, /*reproduce_findings_as_separate_tests=*/true, + /*replay_in_single_process=*/true, + /*print_subprocess_log*/ true, /*stack_limit=*/100, /*rss_limit=*/200, /*time_limit_per_input=*/absl::Seconds(42), diff --git a/fuzztest/internal/googletest_adaptor.cc b/fuzztest/internal/googletest_adaptor.cc index 92264bd8..c4438318 100644 --- a/fuzztest/internal/googletest_adaptor.cc +++ b/fuzztest/internal/googletest_adaptor.cc @@ -36,6 +36,12 @@ std::vector GTest_TestAdaptor::GetFuzzTestsInCurrentShard() const { return result; } +__attribute__((weak)) std::vector ListCrashInputs( + const Configuration& configuration, absl::string_view test_name) { + CorpusDatabase corpus_database(configuration); + return corpus_database.GetCrashingInputsIfAny(test_name); +} + namespace { template void RegisterFuzzTestAsGTest(int* argc, char*** argv, FuzzTest& test, @@ -57,9 +63,9 @@ template void RegisterSeparateRegressionTestForEachCrashingInput( int* argc, char*** argv, FuzzTest& test, const Configuration& configuration) { - CorpusDatabase corpus_database(configuration); + if (!configuration.reproduce_findings_as_separate_tests) return; for (const std::string& input : - corpus_database.GetCrashingInputsIfAny(test.full_name())) { + ListCrashInputs(configuration, test.full_name())) { Configuration updated_configuration = configuration; updated_configuration.crashing_input_to_reproduce = input; const std::string suffix = diff --git a/fuzztest/internal/googletest_adaptor.h b/fuzztest/internal/googletest_adaptor.h index 7286a449..bf685658 100644 --- a/fuzztest/internal/googletest_adaptor.h +++ b/fuzztest/internal/googletest_adaptor.h @@ -40,20 +40,20 @@ class GTest_TestAdaptor : public ::testing::Test { void TestBody() override { auto test = test_.make(); configuration_.fuzz_tests_in_current_shard = GetFuzzTestsInCurrentShard(); + configuration_.replay_in_single_process = + configuration_.crashing_input_to_reproduce.has_value() && + testing::UnitTest::GetInstance()->test_to_run_count() == 1; if (Runtime::instance().run_mode() == RunMode::kUnitTest) { // In "bug reproduction" mode, sometimes we need to reproduce multiple // bugs, i.e., run multiple tests that lead to a crash. bool needs_subprocess = false; -#ifdef GTEST_HAS_DEATH_TEST +#if defined(GTEST_HAS_DEATH_TEST) && !defined(FUZZTEST_USE_CENTIPEDE) needs_subprocess = configuration_.crashing_input_to_reproduce.has_value() && - ( - // When only a single test runs, it's okay to crash the process on - // error, as we don't need to run other tests. - testing::UnitTest::GetInstance()->test_to_run_count() > 1 || - // EXPECT_EXIT is required in the death-test subprocess, but in - // the subprocess there's only one test to run. - testing::internal::InDeathTestChild()); + (!configuration_.replay_in_single_process || + // EXPECT_EXIT is required in the death-test subprocess, but in + // the subprocess there's only one test to run. + testing::internal::InDeathTestChild()); #endif if (needs_subprocess) { configuration_.preprocess_crash_reproducing = [] { @@ -78,7 +78,8 @@ class GTest_TestAdaptor : public ::testing::Test { EXPECT_TRUE(false) << "Death test is not supported."; #endif } else { - test->RunInUnitTestMode(configuration_); + EXPECT_TRUE(test->RunInUnitTestMode(configuration_)) + << "Fuzzing failure in the unit-test mode"; } } else { // TODO(b/245753736): Consider using `tolerate_failure` when FuzzTest can @@ -114,6 +115,9 @@ class GTest_EventListener : public Base { public: void OnTestPartResult(const TestPartResult& test_part_result) override { if (!test_part_result.failed()) return; +#ifdef FUZZTEST_USE_CENTIPEDE + if (getenv("CENTIPEDE_RUNNER_FLAGS") == nullptr) return; +#endif Runtime& runtime = Runtime::instance(); runtime.SetCrashTypeIfUnset("GoogleTest assertion failure"); if (runtime.run_mode() == RunMode::kFuzz) { diff --git a/fuzztest/internal/runtime.cc b/fuzztest/internal/runtime.cc index ecdf0200..825d9a4c 100644 --- a/fuzztest/internal/runtime.cc +++ b/fuzztest/internal/runtime.cc @@ -20,6 +20,7 @@ #endif #include +#include #include #include #include @@ -90,7 +91,11 @@ constexpr size_t kValueMaxPrintLength = 2048; constexpr absl::string_view kTrimIndicator = " ..."; constexpr absl::string_view kReproducerDirName = "fuzztest_repro"; -std::string GetFilterForCrashingInput(absl::string_view crashing_input_path) { +std::string GetFilterForCrashingInput(absl::string_view test_name, + absl::string_view crashing_input_path) { + if (getenv("CENTIPEDE_RUNNER_FLAGS")) { + return absl::StrCat(test_name, "/Regression/", crashing_input_path); + } std::vector dirs = absl::StrSplit(crashing_input_path, '/'); CHECK(dirs.size() > 2) << "Invalid crashing input path!"; return absl::StrCat(dirs[dirs.size() - 3], "/Regression/", dirs.back()); @@ -128,7 +133,7 @@ std::string GetReproductionCommand(const Configuration* configuration, command_template, {{kTestFilterPlaceholder, GetFilterForCrashingInput( - *configuration->crashing_input_to_reproduce)}, + test_name, *configuration->crashing_input_to_reproduce)}, {kExtraArgsPlaceholder, absl::StrJoin(extra_args, " ")}}); } else { return absl::StrReplaceAll( @@ -341,6 +346,13 @@ void Runtime::PrintReport(RawSink out) const { absl::Format(out, "%s", GetSeparator()); } +__attribute__((weak)) void OnRuntimeTerminationRequested() {} + +void Runtime::SetTerminationRequested() { + termination_requested_.store(true, std::memory_order_relaxed); + OnRuntimeTerminationRequested(); +} + void Runtime::StartWatchdog() { // Centipede runner has its own watchdog. #ifndef FUZZTEST_USE_CENTIPEDE @@ -514,7 +526,7 @@ static void SetNewSigAction(int signum, void (*handler)(int, siginfo_t*, void*), } } -void InstallSignalHandlers(FILE* out) { +void InstallSignalHandlers(FILE* out, bool install_crash_handlers) { if (signal_out != nullptr) { // Already installed. Noop. return; @@ -522,23 +534,27 @@ void InstallSignalHandlers(FILE* out) { signal_out = out; #if defined(FUZZTEST_HAS_SANITIZER) - // An ASan failure might come without a signal. - // Eg a divide by zero is intercepted by ASan and it terminates the process - // after printing its output. This handler helps us print our output - // afterwards. - __sanitizer_set_death_callback([](auto...) { - Runtime& runtime = Runtime::instance(); + if (install_crash_handlers) { + // An ASan failure might come without a signal. + // Eg a divide by zero is intercepted by ASan and it terminates the process + // after printing its output. This handler helps us print our output + // afterwards. + __sanitizer_set_death_callback([](auto...) { + Runtime& runtime = Runtime::instance(); #if defined(ADDRESS_SANITIZER) - runtime.SetCrashTypeIfUnset(__asan_get_report_description()); + runtime.SetCrashTypeIfUnset(__asan_get_report_description()); #else - runtime.SetCrashTypeIfUnset("Sanitizer crash"); + runtime.SetCrashTypeIfUnset("Sanitizer crash"); #endif - runtime.PrintReport(&signal_out_sink); - }); + runtime.PrintReport(&signal_out_sink); + }); + } #endif - for (OldSignalHandler& h : crash_handlers) { - SetNewSigAction(h.signum, &HandleCrash, &h.action); + if (install_crash_handlers) { + for (OldSignalHandler& h : crash_handlers) { + SetNewSigAction(h.signum, &HandleCrash, &h.action); + } } for (OldSignalHandler& h : termination_handlers) { @@ -556,7 +572,7 @@ void Runtime::PrintReportOnDefaultSink() const { #else // __linux__ // TODO(sbenzaquen): We should still install signal handlers in other systems. -void InstallSignalHandlers(FILE* out) {} +void InstallSignalHandlers(FILE* out, bool install_crash_handlers) {} void Runtime::PrintFinalStatsOnDefaultSink() const {} @@ -933,7 +949,7 @@ void PopulateLimits(const Configuration& configuration, #endif } -void FuzzTestFuzzerImpl::RunInUnitTestMode(const Configuration& configuration) { +bool FuzzTestFuzzerImpl::RunInUnitTestMode(const Configuration& configuration) { runtime_.SetSkippingRequested(false); fixture_driver_->SetUpFuzzTest(); [&] { @@ -1014,6 +1030,7 @@ void FuzzTestFuzzerImpl::RunInUnitTestMode(const Configuration& configuration) { runtime_.SetCurrentTest(nullptr, nullptr); }(); fixture_driver_->TearDownFuzzTest(); + return true; } FuzzTestFuzzerImpl::RunResult FuzzTestFuzzerImpl::RunOneInput( diff --git a/fuzztest/internal/runtime.h b/fuzztest/internal/runtime.h index 89fd494a..ea8caa91 100644 --- a/fuzztest/internal/runtime.h +++ b/fuzztest/internal/runtime.h @@ -63,7 +63,9 @@ namespace internal { class FuzzTestFuzzer { public: virtual ~FuzzTestFuzzer() = default; - virtual void RunInUnitTestMode(const Configuration& configuration) = 0; + // Returns ture if there no error detected by the FuzzTest (but there can be + // errors reported without using FuzzTest), false otherwise. + virtual bool RunInUnitTestMode(const Configuration& configuration) = 0; // Returns fuzzing mode's exit code. Zero indicates success. virtual int RunInFuzzingMode(int* argc, char*** argv, const Configuration& configuration) = 0; @@ -104,7 +106,7 @@ struct RuntimeStats { size_t max_stack_used; }; -void InstallSignalHandlers(FILE* report_out); +void InstallSignalHandlers(FILE* report_out, bool install_crash_handlers); // A function that is called when crash metadata is available. using CrashMetadataListener = @@ -147,9 +149,7 @@ class Runtime { return should_terminate_on_non_fatal_failure_; } - void SetTerminationRequested() { - termination_requested_.store(true, std::memory_order_relaxed); - } + void SetTerminationRequested(); bool termination_requested() const { return termination_requested_.load(std::memory_order_relaxed); @@ -165,7 +165,7 @@ class Runtime { stats_ = stats; clock_fn_ = clock_fn; // In case we have not installed them yet, do so now. - InstallSignalHandlers(GetStderr()); + InstallSignalHandlers(GetStderr(), /*install_crash_handlers=*/true); ResetCrashType(); } void DisableReporter() { reporter_enabled_ = false; } @@ -280,7 +280,7 @@ class FuzzTestFuzzerImpl : public FuzzTestFuzzer { private: // TODO(fniksic): Refactor to reduce code complexity and improve readability. - void RunInUnitTestMode(const Configuration& configuration) override; + bool RunInUnitTestMode(const Configuration& configuration) override; // TODO(fniksic): Refactor to reduce code complexity and improve readability. int RunInFuzzingMode(int* argc, char*** argv,