blob: 14ff2dd94123e8b048f44cedb0f94806b250bede [file] [log] [blame]
#include "./fuzztest/init_fuzztest.h"
#if defined(__linux__)
#include <unistd.h>
#endif
#include <algorithm>
#include <cctype>
#include <cerrno>
#include <csignal>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "gtest/gtest.h"
#include "absl/algorithm/container.h"
#include "absl/base/no_destructor.h"
#include "absl/container/flat_hash_set.h"
#include "absl/flags/flag.h"
#include "absl/flags/parse.h"
#include "absl/flags/reflection.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "absl/strings/string_view.h"
#include "absl/strings/strip.h"
#include "absl/time/time.h"
#include "./fuzztest/internal/configuration.h"
#include "./fuzztest/internal/flag_name.h"
#include "./fuzztest/internal/googletest_adaptor.h"
#include "./fuzztest/internal/io.h"
#include "./fuzztest/internal/logging.h"
#include "./fuzztest/internal/registry.h"
#include "./fuzztest/internal/runtime.h"
#define FUZZTEST_DEFINE_FLAG(type, name, default_value, description) \
ABSL_FLAG(type, FUZZTEST_FLAG_NAME(name), default_value, description)
FUZZTEST_DEFINE_FLAG(
bool, list_fuzz_tests, false,
"Prints (to stdout) the list of all available FUZZ_TEST-s in the "
"binary and exits. I.e., prints the test names that can be run with "
"the flag `--" FUZZTEST_FLAG_PREFIX "fuzz=<test name>`.")
.OnUpdate([]() {
fuzztest::internal::SetFuzzTestListingModeValidatorForGoogleTest(
absl::GetFlag(FUZZTEST_FLAG(list_fuzz_tests)));
});
static constexpr absl::string_view kUnspecified = "<unspecified>";
FUZZTEST_DEFINE_FLAG(
std::string, fuzz, std::string(kUnspecified),
"Runs a single FUZZ_TEST in continuous fuzzing mode. E.g., "
"`--" FUZZTEST_FLAG_PREFIX
"fuzz=MySuite.MyFuzzTest` runs the given FUZZ_TEST in fuzzing mode. You "
"can also provide just a part of the name, e.g., "
"`--" FUZZTEST_FLAG_PREFIX
"fuzz=MyFuzz`, if it matches only a single FUZZ_TEST. If you have only one "
"fuzz test in your binary, you can also use `--" FUZZTEST_FLAG_PREFIX
"fuzz=` to run it in fuzzing mode (i.e., by setting the flag to empty "
"string). In fuzzing mode the selected test runs until a bug is found or "
"until manually stopped. Fuzzing mode uses coverage feedback to "
"iteratively build up a corpus of inputs that maximize coverage and to "
"reach deep bugs. Note that the binary must be compiled with "
"`--config=fuzztest` for this to work, as it needs coverage "
"instrumentation.");
FUZZTEST_DEFINE_FLAG(
absl::Duration, fuzz_for, absl::InfiniteDuration(),
"Runs all fuzz tests in fuzzing mode up to the specified duration. Can "
"be combined with --" FUZZTEST_FLAG_PREFIX
"fuzz to select a single fuzz tests, or with --" GTEST_FLAG_PREFIX_
"filter to select a subset of fuzz tests. It's recommended to run the "
"tests in the binary in parallel (e.g., "
"https://bazel.build/reference/test-encyclopedia#test-sharding). This flag "
"can be useful for setting up periodic fuzzing in CI. The specified "
"duration is the maximum time used for fuzzing a single FUZZ_TEST or all "
"FUZZ_TESTs in the binary, based on the value of --" FUZZTEST_FLAG_PREFIX
"time_budget_type.");
FUZZTEST_DEFINE_FLAG(
std::string, corpus_database,
"~/.cache/fuzztest",
"The directory containing all corpora for all fuzz tests in the project. "
"For each test binary, there's a corresponding <binary_name> "
"subdirectory in `corpus_database`, and the <binary_name> directory has "
"the following structure: (1) For each fuzz test `SuiteName.TestName` in "
"the binary, there's a sub-directory with the name of that test "
"('<binary_name>/SuiteName.TestName'). (2) For each fuzz test, there are "
"three directories containing `regression`, `crashing`, and `coverage` "
"directories. Files in the `regression` directory will always be used. "
"Files in `crashing` directory will be used when "
"--reproduce_findings_as_separate_tests flag is true. And finally, all "
"files in `coverage` directory will be used when --replay_corpus flag is "
"specified.");
FUZZTEST_DEFINE_FLAG(bool, reproduce_findings_as_separate_tests, false,
"When true, the selected tests replay all crashing inputs "
"in the corpus database as separate TEST-s.");
FUZZTEST_DEFINE_FLAG(
std::string, replay_corpus, std::string(kUnspecified),
"Runs a single FUZZ_TEST in corpus replay mode. E.g., "
"`--" FUZZTEST_FLAG_PREFIX
"replay_corpus=MySuite.MyFuzzTest` replays the corpus from the corpus "
"database for the given FUZZ_TEST. You can also provide just a part of the "
"name, e.g., `--" FUZZTEST_FLAG_PREFIX
"replay_corpus=MyFuzz`, if it matches only a single FUZZ_TEST. If you have "
"only one fuzz test in your binary, you can also use "
"`--" FUZZTEST_FLAG_PREFIX
"replay_corpus=` to run it in the corpus replay mode (i.e., by setting the "
"flag to empty string). In corpus replay mode the selected test replays "
"the inputs that exercise unique coverage (corpus) from the corpus "
"database. Note that this does not include crashing inputs (counterexample "
"findings). Replaying coverage (non-crashing) inputs is useful for "
"measuring the coverage of the corpus built up during previously ran "
"fuzzing sessions, or to catch newly introduced regressions at presubmit "
"time in CI.");
FUZZTEST_DEFINE_FLAG(
absl::Duration, replay_corpus_for, absl::ZeroDuration(),
"Runs all fuzz tests in corpus replay mode up to the specified duration. "
"Can be combined with --" FUZZTEST_FLAG_PREFIX
"fuzz to select a single fuzz test, or with --" GTEST_FLAG_PREFIX_
"filter to select a subset of fuzz tests. To replay the entire corpus for "
"each FUZZ_TEST in the binary, set this flag to `inf` (infinite time "
"budget). It's recommended to run the tests in the binary in parallel "
"(e.g., https://bazel.build/reference/test-encyclopedia#test-sharding). "
"The specified duration is the maximum time used for replaying the corpus "
"for a single FUZZ_TEST or all FUZZ_TESTs in the binary, based on the "
"value of --" FUZZTEST_FLAG_PREFIX "time_budget_type.");
FUZZTEST_DEFINE_FLAG(
fuzztest::internal::TimeBudgetType, time_budget_type,
fuzztest::internal::TimeBudgetType::kPerTest,
"Determines whether the time budget specified by --" FUZZTEST_FLAG_PREFIX
"fuzz_for or --" FUZZTEST_FLAG_PREFIX
"replay_corpus_for is for each FUZZ_TEST or for all 'N' running FUZZ_TESTs "
". In the latter case, each FUZZ_TEST will run for at most (1/N)th of the "
"time budget.");
FUZZTEST_DEFINE_FLAG(
std::optional<std::string>, execution_id, std::nullopt,
"If set, will resume or skip running on the corpus database for tests that "
"are previously run with the same execution ID.");
FUZZTEST_DEFINE_FLAG(
size_t, stack_limit_kb, 128,
"The soft limit of the stack size in kibibytes to abort when "
"the limit is exceeded. 0 indicates no limit.");
FUZZTEST_DEFINE_FLAG(size_t, rss_limit_mb, 0,
"The soft limit of the RSS size in mebibytes to abort "
"when the limit is exceeded. 0 indicates no limit.");
FUZZTEST_DEFINE_FLAG(
absl::Duration, time_limit_per_input, absl::InfiniteDuration(),
"The time limit of the property-function: A timeout bug will be reported "
"for an input if the execution of the property-function with the input "
"takes longer than this time limit.");
FUZZTEST_DEFINE_FLAG(std::optional<size_t>, jobs, std::nullopt,
"The number of fuzzing jobs to run in parallel. If "
"unspecified, the number of jobs is 1.");
FUZZTEST_DEFINE_FLAG(
bool, print_subprocess_log, false,
"If set, print the log of the subprocesses spawned by FuzzTest.");
// Internal flags - not part of the user interface.
//
// These flags are meant to be set only by the parent controller process for its
// child processes.
FUZZTEST_DEFINE_FLAG(
std::optional<std::string>, internal_override_fuzz_test, std::nullopt,
"Internal-only flag - do not use directly. If set, only perform operations "
"for the exact fuzz test regardless of other flags.")
.OnUpdate([] {
FUZZTEST_INTERNAL_CHECK_PRECONDITION(
!absl::GetFlag(FUZZTEST_FLAG(internal_override_fuzz_test))
.has_value() ||
std::getenv("CENTIPEDE_RUNNER_FLAGS") != nullptr,
"must not set --" FUZZTEST_FLAG_PREFIX
"internal_override_fuzz_test directly");
});
FUZZTEST_DEFINE_FLAG(
absl::Duration, internal_override_total_time_limit,
absl::InfiniteDuration(),
"Internal-only flag - do not use directly. If --" FUZZTEST_FLAG_PREFIX
"internal_override_fuzz_test is set, override the time limit set by "
"--" FUZZTEST_FLAG_PREFIX "fuzz_for / --" FUZZTEST_FLAG_PREFIX
"replay_corpus_for with --time_budget_type set to total.")
.OnUpdate([] {
FUZZTEST_INTERNAL_CHECK_PRECONDITION(
absl::GetFlag(FUZZTEST_FLAG(internal_override_total_time_limit)) ==
absl::InfiniteDuration() ||
std::getenv("CENTIPEDE_RUNNER_FLAGS") != nullptr,
"must not set --" FUZZTEST_FLAG_PREFIX
"internal_override_total_time_limit directly");
});
namespace fuzztest {
std::vector<std::string> ListRegisteredTests() {
std::vector<std::string> result;
internal::ForEachTest(
[&](const auto& test) { result.push_back(test.full_name()); });
return result;
}
std::string GetMatchingFuzzTestOrExit(std::string_view name) {
const std::string partial_name(name);
const std::vector<std::string> full_names = ListRegisteredTests();
std::vector<const std::string*> matches;
for (const std::string& full_name : full_names) {
if (absl::StrContains(full_name, partial_name)) {
if (full_name == partial_name) {
// In case of an exact match, we end the search and use it. This is to
// handle the case when we want to select `MySuite.MyTest`, but the
// binary has both `MySuite.MyTest` and `MySuite.MyTestX`.
return full_name;
} else {
matches.push_back(&full_name);
}
}
}
if (matches.empty()) {
absl::FPrintF(stderr, "\n\nNo FUZZ_TEST matches the name: %s\n\n",
partial_name);
absl::FPrintF(stderr, "Valid tests:\n");
for (const std::string& full_name : full_names) {
absl::FPrintF(stderr, " %s\n", full_name);
}
exit(1);
} else if (matches.size() > 1) {
absl::FPrintF(stderr, "\n\nMultiple FUZZ_TESTs match the name: %s\n\n",
partial_name);
absl::FPrintF(stderr, "Please select one. Matching tests:\n");
for (const std::string* full_name : matches) {
absl::FPrintF(stderr, " %s\n", *full_name);
}
exit(1);
}
return *matches[0];
}
namespace {
std::optional<absl::Duration> 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;
}
return fuzz_time_limit;
}
std::optional<absl::Duration> GetReplayCorpusTime() {
absl::Duration replay_corpus_time_limit =
absl::GetFlag(FUZZTEST_FLAG(replay_corpus_for));
if (absl::GetFlag(FUZZTEST_FLAG(replay_corpus)) == kUnspecified &&
replay_corpus_time_limit <= absl::ZeroDuration()) {
return std::nullopt;
}
if (absl::GetFlag(FUZZTEST_FLAG(replay_corpus)) != kUnspecified &&
replay_corpus_time_limit <= absl::ZeroDuration()) {
replay_corpus_time_limit = absl::InfiniteDuration();
}
return replay_corpus_time_limit;
}
internal::Configuration CreateConfigurationsFromFlags(
absl::string_view binary_identifier) {
const bool reproduce_findings_as_separate_tests =
absl::GetFlag(FUZZTEST_FLAG(reproduce_findings_as_separate_tests));
const std::optional<absl::Duration> fuzzing_time_limit = GetFuzzingTime();
const std::optional<absl::Duration> replay_corpus_time_limit =
GetReplayCorpusTime();
const std::optional<std::string> override_fuzz_test =
absl::GetFlag(FUZZTEST_FLAG(internal_override_fuzz_test));
const bool replay_coverage_inputs =
fuzzing_time_limit.has_value() || replay_corpus_time_limit.has_value();
const absl::Duration time_limit =
override_fuzz_test.has_value()
? absl::GetFlag(FUZZTEST_FLAG(internal_override_total_time_limit))
: fuzzing_time_limit.has_value() ? *fuzzing_time_limit
: replay_corpus_time_limit.has_value() ? *replay_corpus_time_limit
: absl::ZeroDuration();
const internal::TimeBudgetType time_budget_type =
override_fuzz_test.has_value()
? internal::TimeBudgetType::kTotal
: absl::GetFlag(FUZZTEST_FLAG(time_budget_type));
const std::optional<size_t> jobs = absl::GetFlag(FUZZTEST_FLAG(jobs));
FUZZTEST_INTERNAL_CHECK(!jobs.has_value() || *jobs > 0, "If specified, --",
FUZZTEST_FLAG(jobs).Name(), " must be positive.");
return internal::Configuration{
absl::GetFlag(FUZZTEST_FLAG(corpus_database)),
/*stats_root=*/"",
std::string(binary_identifier),
/*fuzz_tests=*/ListRegisteredTests(),
/*fuzz_tests_in_current_shard=*/ListRegisteredTests(),
reproduce_findings_as_separate_tests, replay_coverage_inputs,
/*only_replay=*/
replay_corpus_time_limit.has_value(),
absl::GetFlag(FUZZTEST_FLAG(execution_id)),
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,
time_budget_type, jobs.value_or(0)};
}
} // namespace
void RunSpecifiedFuzzTest(std::string_view name, std::string_view binary_id) {
const std::string matching_fuzz_test = GetMatchingFuzzTestOrExit(name);
internal::Configuration configuration =
CreateConfigurationsFromFlags({binary_id.data(), binary_id.size()});
internal::ForEachTest([&](auto& test) {
// TODO(b/301965259): Properly initialize the configuration.
if (test.full_name() == matching_fuzz_test) {
std::exit(test.make()->RunInFuzzingMode(/*argc=*/nullptr,
/*argv=*/nullptr, configuration));
}
});
}
void InitFuzzTest(int* argc, char*** argv, std::string_view binary_id) {
auto& runtime = internal::Runtime::instance();
const bool is_listing = absl::GetFlag(FUZZTEST_FLAG(list_fuzz_tests));
if (is_listing) {
for (const auto& name : ListRegisteredTests()) {
std::cout << "[*] Fuzz test: " << name << '\n';
}
std::exit(0);
}
std::optional<absl::Duration> fuzzing_time_limit = GetFuzzingTime();
std::optional<absl::Duration> replay_corpus_time_limit =
GetReplayCorpusTime();
FUZZTEST_INTERNAL_CHECK(
!fuzzing_time_limit || !replay_corpus_time_limit,
"Cannot run in fuzzing and corpus replay mode at the same time.");
const auto test_to_fuzz = absl::GetFlag(FUZZTEST_FLAG(fuzz));
const auto test_to_replay_corpus =
absl::GetFlag(FUZZTEST_FLAG(replay_corpus));
const auto specified_test = []() -> std::optional<std::string> {
if (auto internal_selected_test =
absl::GetFlag(FUZZTEST_FLAG(internal_override_fuzz_test));
internal_selected_test.has_value()) {
return internal_selected_test;
}
if (auto test_to_fuzz = absl::GetFlag(FUZZTEST_FLAG(fuzz));
test_to_fuzz != kUnspecified) {
return test_to_fuzz;
}
if (auto test_to_replay_corpus =
absl::GetFlag(FUZZTEST_FLAG(replay_corpus));
test_to_replay_corpus != kUnspecified) {
return test_to_replay_corpus;
}
return std::nullopt;
}();
if (specified_test.has_value()) {
const std::string matching_fuzz_test =
GetMatchingFuzzTestOrExit(*specified_test);
// Delegate the test to GoogleTest.
GTEST_FLAG_SET(filter, matching_fuzz_test);
}
std::string derived_binary_id =
binary_id.empty() ? std::string(internal::Basename(*argv[0]))
: std::string(binary_id);
std::optional<std::string> reproduction_command_template;
internal::Configuration configuration =
CreateConfigurationsFromFlags(derived_binary_id);
configuration.reproduction_command_template = reproduction_command_template;
internal::RegisterFuzzTestsAsGoogleTests(argc, argv, configuration);
const bool is_fuzzing_or_replaying =
(fuzzing_time_limit || replay_corpus_time_limit);
if (is_fuzzing_or_replaying && !specified_test.has_value()) {
absl::flat_hash_set<std::string> fuzz_tests = {
configuration.fuzz_tests.begin(), configuration.fuzz_tests.end()};
std::vector<std::string> non_fuzz_tests;
for (const auto* test : internal::GetRegisteredTests()) {
const std::string test_name =
absl::StrCat(test->test_suite_name(), ".", test->name());
if (!fuzz_tests.contains(test_name)) {
non_fuzz_tests.push_back(test_name);
}
}
if (!non_fuzz_tests.empty()) {
// Run only the fuzz tests, and not the unit tests.
// TODO: b/340232436 -- This is needed because we currently rely on a fuzz
// test being the first test to run so that Centipede can get the
// serialized configuration from the binary.
std::string filter = absl::StrCat(
GTEST_FLAG_GET(filter),
// When the filter already includes the negative patterns, append to
// the negative patterns.
absl::StrContains(GTEST_FLAG_GET(filter), '-') ? ":" : "-",
absl::StrJoin(non_fuzz_tests, ":"));
GTEST_FLAG_SET(filter, filter);
}
}
const RunMode run_mode =
fuzzing_time_limit.has_value() ? RunMode::kFuzz : RunMode::kUnitTest;
// TODO(b/307513669): Use the Configuration class instead of Runtime.
runtime.SetRunMode(run_mode);
}
void ParseAbslFlags(int argc, char** argv) {
std::vector<char*> positional_args;
std::vector<absl::UnrecognizedFlag> unrecognized_flags;
absl::ParseAbseilFlagsOnly(argc, argv, positional_args, unrecognized_flags);
}
} // namespace fuzztest