blob: 639d29d24832f1ee69fdb5bdb0ecc92bb8b0402c [file]
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "./fuzztest/internal/runtime.h"
#include "absl/strings/str_split.h"
#if !defined(_WIN32) && !defined(__Fuchsia__)
#define FUZZTEST_HAS_RUSAGE
#include <sys/resource.h>
#endif
#include <algorithm>
#include <cerrno>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <functional>
#include <memory>
#include <optional>
#include <random>
#include <string>
#include <thread> // NOLINT
#include <utility>
#include <vector>
#include "absl/functional/function_ref.h"
#include "absl/log/check.h"
#include "absl/random/bit_gen_ref.h"
#include "absl/random/discrete_distribution.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/string_view.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "absl/types/span.h"
#include "./fuzztest/internal/configuration.h"
#include "./fuzztest/internal/corpus_database.h"
#include "./fuzztest/internal/coverage.h"
#include "./fuzztest/internal/domains/domain_base.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/printer.h"
#include "./fuzztest/internal/serialization.h"
#ifdef ADDRESS_SANITIZER
#include <sanitizer/asan_interface.h>
#endif
#ifndef TRAP_PERF
inline constexpr int TRAP_PERF = 6;
#endif
namespace fuzztest::internal {
using ::fuzztest::domain_implementor::PrintMode;
using ::fuzztest::domain_implementor::RawSink;
void (*crash_handler_hook)();
void Runtime::DumpReproducer(absl::string_view outdir) const {
const std::string content =
current_args_->domain.SerializeCorpus(current_args_->corpus_value)
.ToString();
const std::string filename = WriteDataToDir(content, outdir);
if (filename.empty()) {
absl::FPrintF(GetStderr(), "[!] Failed to write reproducer file.\n");
} else {
absl::FPrintF(GetStderr(), "[*] Reproducer file written to: %s\n",
filename);
}
}
void Runtime::PrintFinalStats(RawSink out) const {
const std::string separator = '\n' + std::string(65, '=') + '\n';
absl::Format(out, "%s=== Fuzzing stats\n\n", separator);
const absl::Duration fuzzing_time = clock_fn_() - stats_->start_time;
absl::Format(out, "Elapsed time: %s\n", absl::FormatDuration(fuzzing_time));
absl::Format(out, "Total runs: %d\n", stats_->runs);
#ifndef FUZZTEST_USE_CENTIPEDE
absl::Format(out, "Edges covered: %d\n", stats_->edges_covered);
absl::Format(out, "Total edges: %d\n", stats_->total_edges);
absl::Format(out, "Corpus size: %d\n", stats_->useful_inputs);
absl::Format(out, "Max stack used: %d\n", stats_->max_stack_used);
#endif
}
namespace {
std::string GetFilterForCrashingInput(absl::string_view crashing_input_path) {
std::vector<std::string> dirs = absl::StrSplit(crashing_input_path, '/');
CHECK(dirs.size() > 2) << "Invalid crashing input path!";
return absl::StrCat(dirs[dirs.size() - 3], "/Regression/", dirs.back());
}
// Returns a reproduction command for replaying
// `configuration.crashing_input_to_reproduce` from a command line, using the
// `configuration.reproduction_command_template`.
std::optional<std::string> GetReproductionCommand(
const Configuration& configuration) {
if (!configuration.reproduction_command_template.has_value()) {
return std::nullopt;
}
if (!configuration.crashing_input_to_reproduce.has_value()) {
return std::nullopt;
}
CHECK(absl::StrContains(*configuration.reproduction_command_template,
kTestFilterPlaceholder));
return absl::StrReplaceAll(
*configuration.reproduction_command_template,
{{kTestFilterPlaceholder,
GetFilterForCrashingInput(
*configuration.crashing_input_to_reproduce)}});
}
constexpr size_t kValueMaxPrintLength = 2048;
constexpr absl::string_view kTrimIndicator = " ...<value too long>";
} // namespace
void Runtime::PrintReport(RawSink out) const {
// We don't want to try and print a fuzz report when we are not running a fuzz
// test, even if we got a crash.
if (!reporter_enabled_) return;
if (auto* coverage = GetExecutionCoverage()) {
// Turn off tracing to avoid having the report trigger more problems during
// tracing, potentially leading to stack overflow.
coverage->SetIsTracing(false);
}
if (crash_handler_hook) crash_handler_hook();
// First, lets try to dump the reproducer if requested.
if (current_args_ != nullptr) {
const char* outdir = getenv("FUZZTEST_REPRODUCERS_OUT_DIR");
if (outdir != nullptr && outdir[0]) {
DumpReproducer(outdir);
}
}
if (run_mode() != RunMode::kUnitTest) {
PrintFinalStats(out);
}
const std::string separator = '\n' + std::string(65, '=') + '\n';
if (current_args_ != nullptr) {
absl::Format(out, "%s=== BUG FOUND!\n\n", separator);
absl::Format(out, "%s:%d: Counterexample found for %s.%s.\n",
current_test_->file(), current_test_->line(),
current_test_->suite_name(), current_test_->test_name());
auto printer = current_args_->domain.GetPrinter();
printer.PrintFormattedAggregateValue(
current_args_->corpus_value, out, PrintMode::kHumanReadable,
/*prefix=*/"The test fails with input:", /*suffix=*/"\n",
/*element_formatter=*/
[](RawSink out, size_t idx, absl::string_view element) {
bool trim = element.size() > kValueMaxPrintLength;
absl::Format(out, "\nargument %d: %s%s", idx,
trim ? element.substr(0, kValueMaxPrintLength) : element,
trim ? kTrimIndicator : "");
});
// There doesn't seem to be a good way to generate a reproducer test when
// the test uses a fixture (see b/241271658).
if (!current_test_->uses_fixture()) {
absl::Format(out, "%s=== Reproducer test\n\n", separator);
absl::Format(out, "TEST(%1$s, %2$sRegression) {\n %2$s(\n",
current_test_->suite_name(), current_test_->test_name());
printer.PrintFormattedAggregateValue(
current_args_->corpus_value, out, PrintMode::kSourceCode,
/*prefix=*/"", /*suffix=*/"",
/*element_formatter=*/
[](RawSink out, size_t idx, absl::string_view element) {
if (idx != 0) absl::Format(out, ",\n");
bool trim = element.size() > kValueMaxPrintLength;
absl::Format(
out, " %s%s",
trim ? element.substr(0, kValueMaxPrintLength) : element,
trim ? kTrimIndicator : "");
});
absl::Format(out, "\n );\n");
absl::Format(out, "}\n");
}
if (current_configuration_ != nullptr) {
const auto reproduction_command =
GetReproductionCommand(*current_configuration_);
if (reproduction_command.has_value()) {
absl::Format(out, "%s=== Reproduction command\n\n", separator);
absl::Format(out, "%s\n\n", *reproduction_command);
}
}
} else {
absl::Format(out, "%s=== SETUP FAILURE!\n\n", separator);
absl::Format(out, "%s:%d: There was a problem with %s.%s.",
current_test_->file(), current_test_->line(),
current_test_->suite_name(), current_test_->test_name());
if (test_abort_message != nullptr) {
absl::Format(out, "%s", *test_abort_message);
}
}
absl::Format(out, "%s", separator);
}
void Runtime::StartWatchdog() {
// Centipede runner has its own watchdog.
#ifndef FUZZTEST_USE_CENTIPEDE
auto watchdog_thread = std::thread(std::bind(&Runtime::Watchdog, this));
while (!watchdog_thread_started) std::this_thread::yield();
watchdog_thread.detach();
#endif
}
void Runtime::Watchdog() {
watchdog_thread_started = true;
while (true) {
while (watchdog_spinlock_.test_and_set()) std::this_thread::yield();
if (test_iteration_started_) CheckWatchdogLimits();
watchdog_spinlock_.clear();
absl::SleepFor(absl::Seconds(1));
}
}
static size_t GetPeakRSSBytes() {
#ifndef FUZZTEST_HAS_RUSAGE
return 0;
#else
struct rusage usage = {};
if (getrusage(RUSAGE_SELF, &usage) != 0) return 0;
// On Linux, ru_maxrss is in KiB
return usage.ru_maxrss * 1024;
#endif
}
void Runtime::CheckWatchdogLimits() {
// Centipede runner has its own watchdog.
#ifndef FUZZTEST_USE_CENTIPEDE
if (current_configuration_ == nullptr) return;
const absl::Duration run_duration =
clock_fn_() - current_iteration_start_time_;
if (current_configuration_->time_limit_per_input > absl::ZeroDuration() &&
run_duration > current_configuration_->time_limit_per_input) {
absl::FPrintF(
GetStderr(), "[!] Per-input timeout exceeded: %s > %s - aborting\n",
absl::FormatDuration(run_duration),
absl::FormatDuration(current_configuration_->time_limit_per_input));
std::abort();
}
const size_t rss_usage = GetPeakRSSBytes();
if (current_configuration_->rss_limit > 0 &&
rss_usage > current_configuration_->rss_limit) {
absl::FPrintF(GetStderr(),
"[!] RSS limit exceeded: %zu > %zu (bytes) - aborting\n",
rss_usage, current_configuration_->rss_limit);
std::abort();
}
#endif
}
void Runtime::OnTestIterationEnd() {
test_iteration_started_ = false;
while (watchdog_spinlock_.test_and_set()) std::this_thread::yield();
CheckWatchdogLimits();
watchdog_spinlock_.clear();
}
#if defined(__linux__)
struct OldSignalHandler {
int signum;
struct sigaction action;
};
static FILE* signal_out;
struct FILESink {
friend void AbslFormatFlush(FILESink*, absl::string_view v) {
fprintf(signal_out, "%.*s", static_cast<int>(v.size()), v.data());
fflush(signal_out);
}
};
static FILESink signal_out_sink;
static OldSignalHandler crash_handlers[] = {{SIGILL}, {SIGFPE}, {SIGSEGV},
{SIGBUS}, {SIGTRAP}, {SIGABRT}};
static OldSignalHandler termination_handlers[] = {
{SIGHUP}, {SIGINT}, {SIGTERM}};
static bool HasCustomHandler(const struct sigaction& sigaction) {
return (sigaction.sa_flags & SA_SIGINFO) ? sigaction.sa_sigaction != nullptr
: (sigaction.sa_handler != nullptr &&
sigaction.sa_handler != SIG_DFL &&
sigaction.sa_handler != SIG_IGN);
}
static void HandleCrash(int signum, siginfo_t* info, void* ucontext) {
// Find the old signal handler.
auto it =
std::find_if(std::begin(crash_handlers), std::end(crash_handlers),
[signum](const auto& h) { return h.signum == signum; });
if (it == std::end(crash_handlers)) {
// signum should never be SIGABRT at this point, but branching on it for
// sanity.
absl::Format(&signal_out_sink,
"[!] HandleCrash called for non-crashing signal %d. This "
"indicates an internal bug! %s",
signum, signum == SIGABRT ? "Exiting" : "Aborting");
if (signum == SIGABRT)
std::_Exit(1);
else
std::abort();
}
const bool has_old_handler = HasCustomHandler(it->action);
// SIGTRAP generated by perf_event_open(sigtrap=1) may be used by
// debugging/analysis tools, so don't consider these as a crash.
if (!has_old_handler || signum != SIGTRAP ||
(info->si_code != TRAP_PERF && info->si_code != SI_TIMER)) {
// Dump our info first.
Runtime::instance().PrintReport(&signal_out_sink);
// The old signal handler might print important messages (e.g., strack
// trace) to the original file descriptors, therefore we restore them before
// calling them.
if (IsSilenceTargetEnabled()) RestoreTargetStdoutAndStderr();
}
if (!has_old_handler) {
// Unblock the signal and invoke the default action if there is no old
// handler.
//
// Note that we treat SIG_IGN as same as SIG_DFL at this point since we
// already reported the crash - if we wanted to ignore the signal we should
// return without reporting.
sigset_t set;
sigemptyset(&set);
sigaddset(&set, signum);
signal(signum, SIG_DFL);
pthread_sigmask(SIG_UNBLOCK, &set, nullptr);
raise(signum);
absl::Format(&signal_out_sink,
"[!] The default action of crashing signal %d did not crash - "
"aborting",
signum);
// At this point abort should be fine even if signum == SIGABRT.
std::abort();
}
if (it->action.sa_flags & SA_SIGINFO) {
it->action.sa_sigaction(signum, info, ucontext);
return;
}
it->action.sa_handler(signum);
}
static void HandleTermination(int, siginfo_t*, void*) {
Runtime::instance().SetTerminationRequested();
}
static void SetNewSigAction(int signum, void (*handler)(int, siginfo_t*, void*),
struct sigaction* old_sigact) {
struct sigaction new_sigact = {};
sigemptyset(&new_sigact.sa_mask);
new_sigact.sa_sigaction = handler;
// We make use of the SA_ONSTACK flag so that signal handlers are executed on
// a separate stack. This is needed to properly handle cases where stack space
// is limited and the delivery of a signal needs to be properly handled.
new_sigact.sa_flags = SA_SIGINFO | SA_ONSTACK;
if (sigaction(signum, &new_sigact, old_sigact) == -1) {
fprintf(GetStderr(), "Error installing signal handler: %s\n",
strerror(errno));
exit(1);
}
}
void InstallSignalHandlers(FILE* out) {
if (signal_out != nullptr) {
// Already installed. Noop.
return;
}
signal_out = out;
#if defined(ADDRESS_SANITIZER) || defined(MEMORY_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::instance().PrintReport(&signal_out_sink); });
#endif
for (OldSignalHandler& h : crash_handlers) {
SetNewSigAction(h.signum, &HandleCrash, &h.action);
}
for (OldSignalHandler& h : termination_handlers) {
SetNewSigAction(h.signum, &HandleTermination, nullptr);
}
}
void Runtime::PrintFinalStatsOnDefaultSink() const {
PrintFinalStats(&signal_out_sink);
}
void Runtime::PrintReportOnDefaultSink() const {
PrintReport(&signal_out_sink);
}
#else // __linux__
// TODO(sbenzaquen): We should still install signal handlers in other systems.
void InstallSignalHandlers(FILE* out) {}
void Runtime::PrintFinalStatsOnDefaultSink() const {}
void Runtime::PrintReportOnDefaultSink() const {}
#endif // __linux__
using corpus_type = GenericDomainCorpusType;
FuzzTestFuzzerImpl::FuzzTestFuzzerImpl(
const FuzzTest& test, std::unique_ptr<UntypedFixtureDriver> fixture_driver)
: test_(test),
fixture_driver_(std::move(fixture_driver)),
params_domain_(fixture_driver_->GetDomains()),
execution_coverage_(internal::GetExecutionCoverage()),
corpus_coverage_(execution_coverage_ != nullptr
? execution_coverage_->GetCounterMap().size()
: 0) {
FUZZTEST_INTERNAL_CHECK_PRECONDITION(fixture_driver_ != nullptr,
"Invalid fixture driver!");
stats_.start_time = absl::Now();
const char* corpus_out_dir_chars = getenv("FUZZTEST_TESTSUITE_OUT_DIR");
if (corpus_out_dir_chars) corpus_out_dir_ = corpus_out_dir_chars;
std::vector<double> weights = {100.};
corpus_distribution_ =
absl::discrete_distribution<>(weights.begin(), weights.end());
}
FuzzTestFuzzerImpl::~FuzzTestFuzzerImpl() {
Runtime::instance().DisableReporter();
}
std::optional<corpus_type> FuzzTestFuzzerImpl::TryParse(
absl::string_view data) {
auto ir_value = IRObject::FromString(data);
if (!ir_value) {
absl::FPrintF(GetStderr(), "[!] Unexpected file format.\n");
return std::nullopt;
}
auto corpus_value = params_domain_.ParseCorpus(*ir_value);
if (!corpus_value) {
absl::FPrintF(GetStderr(), "[!] Unexpected intermediate representation.\n");
return std::nullopt;
}
absl::Status is_valid = params_domain_.ValidateCorpusValue(*corpus_value);
if (!is_valid.ok()) {
absl::FPrintF(GetStderr(), "[!] Invalid corpus value: %s\n",
is_valid.ToString());
return std::nullopt;
}
return corpus_value;
}
std::optional<GenericDomainCorpusType>
FuzzTestFuzzerImpl::GetCorpusValueFromFile(const std::string& path) {
const auto content = ReadFile(path);
if (!content) {
absl::FPrintF(GetStderr(),
"[!] Failed to read file or directory (might be empty): %s\n",
path);
return std::nullopt;
}
auto corpus_value = TryParse(*content);
if (!corpus_value) {
absl::FPrintF(GetStderr(),
"[!] Skipping invalid input file %s.\n===\n%s\n===\n", path,
*content);
}
return corpus_value;
}
void FuzzTestFuzzerImpl::ReplayInput(const std::string& path) {
auto corpus_value = GetCorpusValueFromFile(path);
if (!corpus_value) return;
absl::FPrintF(GetStderr(), "[.] Replaying %s\n", path);
RunOneInput({*corpus_value});
}
bool FuzzTestFuzzerImpl::ReplayInputsIfAvailable(
const Configuration& configuration) {
// Crashing inputs are discovered in fuzzing mode. To increase the chance of
// reproducing the crash, fuzzing mode should be used.
runtime_.SetRunMode(RunMode::kFuzz);
if (const auto file_paths = GetFilesToReplay()) {
for (const std::string& path : *file_paths) {
ReplayInput(path);
}
return true;
}
if (configuration.crashing_input_to_reproduce.has_value()) {
configuration.preprocess_crash_reproducing();
ReplayInput(*configuration.crashing_input_to_reproduce);
return true;
}
if (const auto to_minimize = ReadReproducerToMinimize()) {
absl::FPrintF(GetStderr(),
"[!] Looking for smaller mutations indefinitely: please "
"terminate the process manually (Ctrl-C) after some time.\n");
PRNG prng(seed_sequence_);
const auto original_serialized =
params_domain_.SerializeCorpus(*to_minimize).ToString();
// In minimize mode we keep mutating the given reproducer value with
// `only_shrink=true` until we crash. We drop mutations that don't
// actually change the value.
// That way any crash is a smaller reproduction case.
// We start with a lot of mutations which can speed up minimization.
// We reduce the number of mutations if we can't find a crash, similar to
// simulated annealing.
int num_mutations = 20;
int counter = 0;
while (!ShouldStop()) {
auto copy = *to_minimize;
for (int i = 0; i < num_mutations; ++i) {
params_domain_.Mutate(copy, prng, true);
}
num_mutations = std::max(1, num_mutations - 1);
// We compare the serialized version. Not very efficient but works for
// now.
if (params_domain_.SerializeCorpus(copy).ToString() ==
original_serialized) {
continue;
}
RunOneInput({std::move(copy)});
counter++;
if (counter == 100000) {
absl::FPrintF(GetStderr(), ".");
counter = 0;
}
}
std::exit(130); // Exit code termination.
}
return false;
}
std::optional<std::vector<std::string>> FuzzTestFuzzerImpl::GetFilesToReplay() {
auto file_or_dir = absl::NullSafeStringView(getenv("FUZZTEST_REPLAY"));
if (file_or_dir.empty()) return std::nullopt;
// Try as a directory path first.
std::vector<std::string> files = ListDirectory(std::string(file_or_dir));
// If not, consider it a file path.
if (files.empty()) {
files.push_back(std::string(file_or_dir));
}
std::sort(files.begin(), files.end());
return files;
}
std::optional<corpus_type> FuzzTestFuzzerImpl::ReadReproducerToMinimize() {
auto file = absl::NullSafeStringView(getenv("FUZZTEST_MINIMIZE_REPRODUCER"));
if (file.empty()) return std::nullopt;
absl::FPrintF(GetStderr(), "[*] Minimizing reproducer: %s\n", file);
auto data = ReadFile(std::string(file));
if (!data) {
FUZZTEST_INTERNAL_CHECK(false, "Failed to read minimizer file!");
}
auto res = TryParse(*data);
if (!res) {
absl::FPrintF(GetStderr(), "[!] Invalid input file %s.\n===\n%s\n===\n",
file, *data);
FUZZTEST_INTERNAL_CHECK(false, "Failed to read minimizer file!");
}
return res;
}
void FuzzTestFuzzerImpl::MutateValue(Input& input, absl::BitGenRef prng) {
// Do a random number of mutations on the value at once, skewed
// towards 1 and decreasing probability as we go up.
// Doing multiple smaller mutations at once allows reaching states that
// require larger mutations.
// The current implementation with a Poisson distribution has
// probabilities:
// - 1 mutation: 0.368
// - 2 mutations: 0.368
// - 3 mutations: 0.184
// - 4 mutations: 0.061
// - 5 mutations: 0.015
// - 6 mutations: 0.003
// ...
// The distribution and parameters have not been benchmarked or
// optimized in any significant way.
for (int mutations_at_once = absl::Poisson<int>(prng) + 1;
mutations_at_once > 0; --mutations_at_once) {
params_domain_.Mutate(input.args, prng, /* only_shrink= */ false);
}
}
void FuzzTestFuzzerImpl::UpdateCorpusDistribution() {
std::vector<double> weights(corpus_.size());
absl::Duration average_time = absl::ZeroDuration();
for (const Input& i : corpus_) {
average_time += i.run_time;
}
average_time /= corpus_.size();
// Prefer faster inputs than slower inputs, the maximum bias
// is 30x. These weights are dynamic and won't make slow-but-interesting
// inputs neglected: As more and more "slow but touched new coverage"
// inputs come in, the average execution time will be larger and slow input
// will get higher weights.
for (size_t i = 0; i < corpus_.size(); ++i) {
weights[i] = 100;
if (corpus_[i].run_time > average_time * 10)
weights[i] = 10;
else if (corpus_[i].run_time > average_time * 4)
weights[i] = 25;
else if (corpus_[i].run_time > average_time * 2)
weights[i] = 50;
else if (corpus_[i].run_time * 3 > average_time * 4)
weights[i] = 75;
else if (corpus_[i].run_time * 4 < average_time)
weights[i] = 300;
else if (corpus_[i].run_time * 3 < average_time)
weights[i] = 200;
else if (corpus_[i].run_time * 2 < average_time)
weights[i] = 150;
}
corpus_distribution_ =
absl::discrete_distribution<>(weights.begin(), weights.end());
}
FuzzTestFuzzerImpl::RunResult FuzzTestFuzzerImpl::TrySample(
const Input& sample, bool write_to_file) {
RunResult run_result = RunOneInput(sample);
if (runtime_.external_failure_detected()) {
// We detected a non fatal failure. Record it separately to minimize it
// locally.
minimal_non_fatal_counterexample_ = sample;
}
if (!run_result.new_coverage) return run_result;
if (write_to_file) TryWriteCorpusFile(sample);
++stats_.useful_inputs;
stats_.edges_covered = corpus_coverage_.GetNumberOfCoveredEdges();
const absl::Duration fuzzing_time = absl::Now() - stats_.start_time;
const int runs_per_sec =
static_cast<int>(stats_.runs / absl::ToDoubleSeconds(fuzzing_time));
absl::FPrintF(GetStderr(),
"[*] Corpus size: %5d | Edges covered: %6d | "
"Fuzzing time: %16s | Total runs: %1.2e | Runs/secs: %5d | "
"Max stack usage: %8d\n",
stats_.useful_inputs, stats_.edges_covered,
absl::FormatDuration(fuzzing_time), stats_.runs, runs_per_sec,
stats_.max_stack_used);
return run_result;
}
void FuzzTestFuzzerImpl::TrySampleAndUpdateInMemoryCorpus(Input sample,
bool write_to_file) {
auto [new_coverage, run_time] = TrySample(sample, write_to_file);
if (execution_coverage_ != nullptr &&
(stats_.runs % 4096 == 0 || new_coverage)) {
params_domain_.UpdateMemoryDictionary(sample.args);
}
if (!new_coverage) return;
// New coverage, update corpus and weights.
sample.run_time = run_time;
corpus_.push_back(std::move(sample));
UpdateCorpusDistribution();
}
void FuzzTestFuzzerImpl::ForEachInputFile(
absl::Span<const std::string> files,
absl::FunctionRef<void(Input&&)> consume) {
int parsed_input_counter = 0;
int invalid_input_counter = 0;
for (const auto& path : files) {
std::optional<std::string> data = ReadFile(path);
if (!data) continue;
if (auto corpus_value = TryParse(*data)) {
++parsed_input_counter;
consume(Input{*std::move(corpus_value)});
} else {
++invalid_input_counter;
absl::FPrintF(GetStderr(), "[!] Invalid input file %s.\n", path);
}
}
absl::FPrintF(GetStderr(),
"[*] Parsed %d inputs and ignored %d inputs from the test "
"suite input dir.\n",
parsed_input_counter, invalid_input_counter);
}
bool FuzzTestFuzzerImpl::MinimizeCorpusIfInMinimizationMode(
absl::BitGenRef prng) {
auto inputdir =
absl::NullSafeStringView(getenv("FUZZTEST_MINIMIZE_TESTSUITE_DIR"));
if (inputdir.empty()) return false;
std::vector<std::string> files = ListDirectory(std::string(inputdir));
// Shuffle to potentially improve previously minimized corpus.
std::shuffle(files.begin(), files.end(), prng);
ForEachInputFile(files, [this](Input&& input) {
TrySample(input, /*write_to_file=*/true);
});
return true;
}
std::vector<FuzzTestFuzzerImpl::Input>
FuzzTestFuzzerImpl::TryReadCorpusFromFiles() {
std::vector<Input> inputs;
auto inputdir = absl::NullSafeStringView(getenv("FUZZTEST_TESTSUITE_IN_DIR"));
if (inputdir.empty()) return inputs;
std::vector<std::string> files = ListDirectory(std::string(inputdir));
ForEachInputFile(
files, [&inputs](Input&& input) { inputs.push_back(std::move(input)); });
return inputs;
}
void FuzzTestFuzzerImpl::TryWriteCorpusFile(const Input& input) {
if (corpus_out_dir_.empty()) return;
if (WriteDataToDir(params_domain_.SerializeCorpus(input.args).ToString(),
corpus_out_dir_)
.empty()) {
absl::FPrintF(GetStderr(), "[!] Failed to write corpus file.\n");
}
}
void FuzzTestFuzzerImpl::InitializeCorpus(absl::BitGenRef prng) {
std::vector<Input> inputs = TryReadCorpusFromFiles();
// Since inputs processed earlier have the adventage of increasing coverage
// and being kept in corpus, shuffle the input order to make it fair.
std::shuffle(inputs.begin(), inputs.end(), prng);
for (auto& input : inputs) {
TrySampleAndUpdateInMemoryCorpus(std::move(input),
/*write_to_file=*/false);
}
if (corpus_.empty()) {
TrySampleAndUpdateInMemoryCorpus(Input{params_domain_.Init(prng)});
}
}
bool FuzzTestFuzzerImpl::ShouldStop() {
if (runs_limit_.has_value() && stats_.runs >= *runs_limit_) return true;
if (time_limit_ != absl::InfiniteFuture() && absl::Now() > time_limit_)
return true;
return runtime_.termination_requested();
}
void FuzzTestFuzzerImpl::PopulateFromSeeds(
const std::vector<std::string>& corpus_files) {
for (const auto& seed : fixture_driver_->GetSeeds()) {
TrySampleAndUpdateInMemoryCorpus(
Input{seed},
// Dump the seed to the corpus so that it is present when the corpus is
// used in minimization or coverage replay.
/*write_to_file=*/true);
}
for (const auto& corpus_file : corpus_files) {
auto seed = GetCorpusValueFromFile(corpus_file);
if (!seed) continue;
TrySampleAndUpdateInMemoryCorpus(
Input{*seed},
// Dump the seed to the corpus so that it is present when the corpus is
// used in minimization or coverage replay.
/*write_to_file=*/true);
}
}
size_t GetStackLimitFromEnvOrConfiguration(const Configuration& configuration) {
size_t env_stack_limit;
if (const char* env = getenv("FUZZTEST_STACK_LIMIT");
(env != nullptr && absl::SimpleAtoi(env, &env_stack_limit))) {
absl::FPrintF(
GetStderr(),
"[!] Stack limit is set by FUZZTEST_STACK_LIMIT env var - this is "
"going to be deprecated soon. Consider switching to "
"--" FUZZTEST_FLAG_PREFIX "stack_limit_kb flag.\n");
return env_stack_limit;
}
return configuration.stack_limit;
}
void PopulateLimits(const Configuration& configuration,
ExecutionCoverage* execution_coverage) {
// centipede_adaptor would populate the limits to Centipede.
#ifndef FUZZTEST_USE_CENTIPEDE
// TODO(b/273276918): For now, let existing FUZZTEST_STACK_LIMIT overwrite the
// stack limit. So that the existing targets that set the env var could still
// work.
if (execution_coverage)
execution_coverage->SetStackLimit(
GetStackLimitFromEnvOrConfiguration(configuration));
#endif
}
void FuzzTestFuzzerImpl::RunInUnitTestMode(const Configuration& configuration) {
fixture_driver_->SetUpFuzzTest();
runtime_.StartWatchdog();
PopulateLimits(configuration, execution_coverage_);
[&] {
runtime_.EnableReporter(&stats_, [] { return absl::Now(); });
runtime_.SetCurrentTest(&test_, &configuration);
// TODO(sbenzaquen): Currently, some infrastructure code assumes that replay
// works in unit test mode, so we support it. However, we would like to
// limit replaying to fuzzing mode only, where we can guarantee that only
// a single FUZZ_TEST is selected to run. Once we make sure that no
// existing infra tries to replay in unit test mode, we can remove this.
if (ReplayInputsIfAvailable(configuration)) {
// If ReplayInputs returns, it means the replay didn't crash.
// In replay mode, we only replay.
runtime_.DisableReporter();
return;
}
CorpusDatabase corpus_database(configuration);
for (const std::string& file :
corpus_database.GetRegressionInputs(test_.full_name())) {
ReplayInput(file);
}
for (const std::string& file :
corpus_database.GetCoverageInputsIfAny(test_.full_name())) {
ReplayInput(file);
}
runtime_.SetRunMode(RunMode::kUnitTest);
// If crashing inputs are reported, there's no need for a smoke test.
if (corpus_database.use_crashing_inputs()) return;
PopulateFromSeeds(/*corpus_files=*/{});
auto duration = absl::Seconds(1);
const auto fuzz_for = absl::NullSafeStringView(getenv("FUZZTEST_FUZZ_FOR"));
if (!fuzz_for.empty()) {
FUZZTEST_INTERNAL_CHECK(
absl::ParseDuration(fuzz_for, &duration),
"Could not parse duration in FUZZTEST_FUZZ_FOR=", fuzz_for);
}
const auto time_limit = stats_.start_time + duration;
PRNG prng(seed_sequence_);
Input mutation{params_domain_.Init(prng)};
constexpr size_t max_iterations = 10000;
for (int i = 0; i < max_iterations; ++i) {
runtime_.SetExternalFailureDetected(false);
RunOneInput(mutation);
if (runtime_.external_failure_detected()) {
break;
}
// We mutate the value, except that every num_mutations_per_value we
// generate a new one through Init.
constexpr size_t num_mutations_per_value = 100;
if (i % num_mutations_per_value < num_mutations_per_value - 1) {
MutateValue(mutation, prng);
} else {
mutation.args = params_domain_.Init(prng);
}
if (absl::Now() > time_limit) {
// Break the test after 1 second of running to avoid time outs on
// unittests when the fuzz test is doing a lot of work in a single
// iteration.
break;
}
}
runtime_.SetCurrentTest(nullptr, nullptr);
}();
fixture_driver_->TearDownFuzzTest();
}
FuzzTestFuzzerImpl::RunResult FuzzTestFuzzerImpl::RunOneInput(
const Input& input) {
++stats_.runs;
auto untyped_args = params_domain_.GetValue(input.args);
Runtime::Args debug_args{input.args, params_domain_};
runtime_.SetCurrentArgs(&debug_args);
// Reset and observe the coverage map and start tracing in
// the tightest scope possible. In particular, we can't include the call
// to GetValue in the scope as it will run user code.
if (execution_coverage_ != nullptr) {
execution_coverage_->ResetState();
}
absl::Time start = absl::Now();
runtime_.OnTestIterationStart(start);
// Set tracing after absl::Now(), otherwise it will make
// FuzzingModeTest.MinimizesDuplicatedCorpustest flaky because
// randomness in absl::Now() being traced by cmp coverage.
if (execution_coverage_ != nullptr) {
execution_coverage_->SetIsTracing(true);
}
fixture_driver_->SetUpIteration();
fixture_driver_->Test(std::move(untyped_args));
fixture_driver_->TearDownIteration();
if (execution_coverage_ != nullptr) {
execution_coverage_->SetIsTracing(false);
}
const absl::Duration run_time = absl::Now() - start;
bool new_coverage = false;
if (execution_coverage_ != nullptr) {
new_coverage = corpus_coverage_.Update(execution_coverage_);
stats_.max_stack_used =
std::max(stats_.max_stack_used, execution_coverage_->MaxStackUsed());
}
runtime_.OnTestIterationEnd();
runtime_.UnsetCurrentArgs();
return {new_coverage, run_time};
}
void FuzzTestFuzzerImpl::MinimizeNonFatalFailureLocally(absl::BitGenRef prng) {
// We try to minimize the counterexample until we reach a point where no new
// failures are found.
// We stop when run kMaxTriedWithoutFailure consecutive runs without finding a
// smaller failure, but also add a time limit in case each iteration takes too
// long.
const absl::Time deadline =
std::min(absl::Now() + absl::Minutes(1), time_limit_);
int tries_without_failure = 0;
constexpr int kMaxTriedWithoutFailure = 10000;
while (tries_without_failure < kMaxTriedWithoutFailure &&
absl::Now() < deadline) {
FUZZTEST_INTERNAL_CHECK(
minimal_non_fatal_counterexample_.has_value(),
"Caller didn't populate minimal_non_fatal_counterexample_");
auto copy = *minimal_non_fatal_counterexample_;
// Mutate a random number of times, in case one is not enough to
// reach another failure, but prefer a low number of mutations (thus Zipf).
for (int num_mutations = absl::Zipf(prng, 10); num_mutations >= 0;
--num_mutations) {
params_domain_.Mutate(copy.args, prng, /* only_shrink= */ true);
}
// Only run it if it actually is different. Random mutations might
// not actually change the value, or we have reached a minimum that can't be
// minimized anymore.
if (params_domain_.SerializeCorpus(minimal_non_fatal_counterexample_->args)
.ToString() !=
params_domain_.SerializeCorpus(copy.args).ToString()) {
runtime_.SetExternalFailureDetected(false);
RunOneInput(copy);
if (runtime_.external_failure_detected()) {
// Found a smaller one, record it and reset the counter.
minimal_non_fatal_counterexample_ = std::move(copy);
tries_without_failure = 0;
continue;
}
}
++tries_without_failure;
}
}
int FuzzTestFuzzerImpl::RunInFuzzingMode(int* /*argc*/, char*** /*argv*/,
const Configuration& configuration) {
fixture_driver_->SetUpFuzzTest();
runtime_.StartWatchdog();
PopulateLimits(configuration, execution_coverage_);
const int exit_code = [&] {
runtime_.SetRunMode(RunMode::kFuzz);
if (IsSilenceTargetEnabled()) SilenceTargetStdoutAndStderr();
runtime_.EnableReporter(&stats_, [] { return absl::Now(); });
runtime_.SetCurrentTest(&test_, &configuration);
if (ReplayInputsIfAvailable(configuration)) {
// If ReplayInputs returns, it means the replay didn't crash.
// We don't want to actually run the fuzzer so exit now.
return 0;
}
if (execution_coverage_ == nullptr) {
absl::FPrintF(
GetStderr(),
"\n\n[!] To fuzz, please build with --config=fuzztest.\n\n\n");
return 1;
}
stats_.total_edges = execution_coverage_->GetCounterMap().size();
PRNG prng(seed_sequence_);
if (MinimizeCorpusIfInMinimizationMode(prng)) {
absl::FPrintF(
GetStderr(),
"[*] Selected %d corpus inputs in minimization mode - exiting.\n",
stats_.useful_inputs);
return 0;
}
CorpusDatabase corpus_database(configuration);
PopulateFromSeeds(
corpus_database.GetCoverageInputsIfAny(test_.full_name()));
InitializeCorpus(prng);
FUZZTEST_INTERNAL_CHECK(!corpus_.empty(),
"No seed input coverage registered. Test function "
"might be uninstrumented?");
constexpr size_t kRunsPerInit = 32000;
size_t next_init = kRunsPerInit;
if (const char* max_fuzzing_runs_env =
getenv("FUZZTEST_MAX_FUZZING_RUNS")) {
if (size_t max_fuzzing_runs;
absl::SimpleAtoi(max_fuzzing_runs_env, &max_fuzzing_runs)) {
absl::FPrintF(GetStderr(), "[.] Limiting to %d fuzzing runs.\n",
max_fuzzing_runs);
runs_limit_ = stats_.runs + max_fuzzing_runs;
} else {
absl::FPrintF(GetStderr(),
"[!] Failed to parse FUZZTEST_MAX_FUZZING_RUNS as "
"non-negative integer - will not limit fuzzing runs.\n");
}
}
if (configuration.time_limit_per_test != absl::InfiniteDuration()) {
absl::FPrintF(GetStderr(), "[.] Fuzzing timeout set to: %s\n",
absl::FormatDuration(configuration.time_limit_per_test));
time_limit_ = stats_.start_time + configuration.time_limit_per_test;
}
runtime_.SetShouldTerminateOnNonFatalFailure(false);
auto try_input_and_process_counterexample = [&](Input input) -> void {
TrySampleAndUpdateInMemoryCorpus(std::move(input));
if (minimal_non_fatal_counterexample_.has_value()) {
// We found a failure, let's minimize it here.
MinimizeNonFatalFailureLocally(prng);
// Once we have minimized enough, let it crash with the best sample we
// got.
// TODO(sbenzaquen): Consider a different approach where we don't retry
// the failing sample to force a crash. Instead, we could store the
// information from the first failure and generate a report manually.
runtime_.SetShouldTerminateOnNonFatalFailure(true);
runtime_.SetExternalFailureDetected(false);
RunOneInput(*minimal_non_fatal_counterexample_);
}
};
// First briefly try the initial values to account for seeded domains and
// possible special values.
constexpr int kInitialValuesToTry = 32;
for (int i = 0; i < kInitialValuesToTry && !ShouldStop(); ++i) {
try_input_and_process_counterexample({params_domain_.Init(prng)});
}
// Fuzz corpus elements in round robin fashion.
while (!ShouldStop()) {
Input input_to_mutate = [&]() -> Input {
// Try a brand new random element every now and then.
// Otherwise, go to next corpus element in queue.
if (stats_.runs > next_init) {
next_init = stats_.runs + kRunsPerInit;
return {params_domain_.Init(prng)};
} else {
size_t idx = static_cast<size_t>(corpus_distribution_(prng));
FUZZTEST_INTERNAL_CHECK(0 <= idx && idx < corpus_.size(),
"Corpus input weights are outdated!\n");
return corpus_[idx];
}
}();
constexpr int kMutationsPerInput = 32;
for (int i = 0; i < kMutationsPerInput; ++i) {
if (ShouldStop()) break;
Input mutation = input_to_mutate;
MutateValue(mutation, prng);
try_input_and_process_counterexample(std::move(mutation));
}
}
runtime_.SetCurrentTest(nullptr, nullptr);
absl::FPrintF(GetStderr(), "\n[.] Fuzzing was terminated.\n");
runtime_.PrintFinalStatsOnDefaultSink();
absl::FPrintF(GetStderr(), "\n");
return 0;
}();
fixture_driver_->TearDownFuzzTest();
return exit_code;
}
} // namespace fuzztest::internal