Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions centipede/centipede_flags.inc
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,12 @@ CENTIPEDE_FLAG(std::string, fuzztest_workdir_root, "",
CENTIPEDE_FLAG(bool, fuzztest_only_replay, false,
"If set, further steps are skipped after replaying with the "
"corpus database.")
CENTIPEDE_FLAG(
std::optional<bool>, fuzztest_update_corpus_database, std::nullopt,
"If true, update the corpus database (both crashes and coverage). "
"If false, do not update it. "
"If unspecified, it defaults to true in fuzzing mode and false "
"in replay mode.")
CENTIPEDE_FLAG(
std::string, fuzztest_execution_id, "",
"If set, will be used when working on a corpus database to resume "
Expand Down
152 changes: 91 additions & 61 deletions centipede/centipede_interface.cc
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,90 @@ PeriodicAction RecordFuzzingTime(std::string_view fuzzing_time_file,
PeriodicAction::ZeroDelayConstInterval(absl::Seconds(15))};
}

struct DatabasePaths {
std::filesystem::path fuzztest_db_path;
std::filesystem::path regression_dir;
std::filesystem::path coverage_dir;
std::filesystem::path crashing_dir;
std::filesystem::path incubating_dir;
};

DatabasePaths GetDatabasePaths(const Environment& env) {
const auto corpus_database_path =
std::filesystem::path(env.fuzztest_corpus_database) /
env.fuzztest_binary_identifier;
const std::filesystem::path fuzztest_db_path =
corpus_database_path / env.test_name;
return {
fuzztest_db_path,
fuzztest_db_path / "regression",
fuzztest_db_path / "coverage",
fuzztest_db_path / "crashing",
fuzztest_db_path / "incubating",
};
}

void UpdateCorpus(const Environment& env, const DatabasePaths& db_paths,
CentipedeCallbacksFactory& callbacks_factory,
StopCondition& stop_condition) {
const WorkDir workdir{env};

// The test time limit does not apply for the rest of the steps.
stop_condition.SetStopTime(absl::InfiniteFuture());

const bool update_crashing_db =
env.fuzztest_update_corpus_database.value_or(!env.fuzztest_only_replay);
const bool update_coverage_db =
update_crashing_db && !env.fuzztest_only_replay;
if (!update_crashing_db && !env.report_crash_summary) {
return;
}

// Deduplicate and optionally update the crashing inputs.
CrashSummary crash_summary{env.fuzztest_binary_identifier, env.test_name};
const absl::flat_hash_map<std::string, CrashDetails> crashes_by_signature =
GetCrashesFromWorkdir(workdir, env.total_shards);
if (update_crashing_db) {
const absl::Status status = OrganizeCrashingInputs(
db_paths.regression_dir, db_paths.crashing_dir, db_paths.incubating_dir,
env, callbacks_factory, crashes_by_signature, crash_summary,
stop_condition, kDefaultRegressionTtl);
FUZZTEST_LOG_IF(ERROR, !status.ok())
<< "Failed to organize crashing inputs: " << status;
if (env.report_crash_summary) crash_summary.Report(&std::cerr);
} else if (env.report_crash_summary) {
// Just report the crashes.
for (const auto& [crash_signature, crash_details] : crashes_by_signature) {
crash_summary.AddCrash({/*id=*/crash_details.input_signature,
/*category=*/crash_details.description,
crash_signature, crash_details.description});
}
crash_summary.Report(&std::cerr);
}

if (update_coverage_db) {
// Distill and store the coverage corpus.
Distill(env);
if (RemotePathExists(db_paths.coverage_dir.c_str())) {
// In the future, we will store k latest coverage corpora for some k, but
// for now we only keep the latest one.
FUZZTEST_CHECK_OK(RemotePathDelete(db_paths.coverage_dir.c_str(),
/*recursively=*/true));
}
FUZZTEST_CHECK_OK(RemoteMkdir(db_paths.coverage_dir.c_str()));
std::vector<std::string> distilled_corpus_files;
FUZZTEST_CHECK_OK(
RemoteGlobMatch(workdir.DistilledCorpusFilePaths().AllShardsGlob(),
distilled_corpus_files));
for (const std::string& corpus_file : distilled_corpus_files) {
const std::string file_name =
std::filesystem::path(corpus_file).filename();
FUZZTEST_CHECK_OK(RemoteFileRename(
corpus_file, (db_paths.coverage_dir / file_name).c_str()));
}
}
}

void UpdateCorpusDatabase(Environment env,
CentipedeCallbacksFactory& callbacks_factory,
StopCondition& stop_condition) {
Expand Down Expand Up @@ -455,18 +539,16 @@ void UpdateCorpusDatabase(Environment env,
}
};

const std::filesystem::path fuzztest_db_path =
corpus_database_path / env.test_name;
const std::filesystem::path regression_dir = fuzztest_db_path / "regression";
const std::filesystem::path coverage_dir = fuzztest_db_path / "coverage";
const DatabasePaths db_paths = GetDatabasePaths(env);

// Seed the fuzzing session with the latest coverage corpus and regression
// inputs from the previous fuzzing session.
if (!is_resuming) {
FUZZTEST_CHECK_OK(GenerateSeedCorpusFromConfig(
GetSeedCorpusConfig(
env, regression_dir.c_str(),
env.fuzztest_replay_coverage_inputs ? coverage_dir.c_str() : ""),
GetSeedCorpusConfig(env, db_paths.regression_dir.c_str(),
env.fuzztest_replay_coverage_inputs
? db_paths.coverage_dir.c_str()
: ""),
env.binary_name, env.binary_hash))
<< "while generating the seed corpus";
}
Expand Down Expand Up @@ -524,57 +606,7 @@ void UpdateCorpusDatabase(Environment env,
}
return;
}
// The test time limit does not apply for the rest of the steps.
stop_condition.SetStopTime(absl::InfiniteFuture());

// TODO(xinhaoyuan): Have a separate flag to skip corpus updating instead
// of checking whether workdir is specified or not.
const bool skip_corpus_db_update =
env.fuzztest_only_replay || is_workdir_specified;
if (skip_corpus_db_update && !env.report_crash_summary) return;

// Deduplicate and optionally update the crashing inputs.
CrashSummary crash_summary{env.fuzztest_binary_identifier, env.test_name};
const absl::flat_hash_map<std::string, CrashDetails> crashes_by_signature =
GetCrashesFromWorkdir(workdir, env.total_shards);
if (skip_corpus_db_update) {
// Just report the crashes.
FUZZTEST_CHECK(env.report_crash_summary);
for (const auto& [crash_signature, crash_details] : crashes_by_signature) {
crash_summary.AddCrash({/*id=*/crash_details.input_signature,
/*category=*/crash_details.description,
crash_signature, crash_details.description});
}
crash_summary.Report(&std::cerr);
return;
}
const absl::Status status = OrganizeCrashingInputs(
regression_dir, fuzztest_db_path / "crashing",
fuzztest_db_path / "incubating", env, callbacks_factory,
crashes_by_signature, crash_summary, stop_condition,
kDefaultRegressionTtl);
FUZZTEST_LOG_IF(ERROR, !status.ok())
<< "Failed to organize crashing inputs: " << status;
if (env.report_crash_summary) crash_summary.Report(&std::cerr);

// Distill and store the coverage corpus.
Distill(env);
if (RemotePathExists(coverage_dir.c_str())) {
// In the future, we will store k latest coverage corpora for some k, but
// for now we only keep the latest one.
FUZZTEST_CHECK_OK(
RemotePathDelete(coverage_dir.c_str(), /*recursively=*/true));
}
FUZZTEST_CHECK_OK(RemoteMkdir(coverage_dir.c_str()));
std::vector<std::string> distilled_corpus_files;
FUZZTEST_CHECK_OK(
RemoteGlobMatch(workdir.DistilledCorpusFilePaths().AllShardsGlob(),
distilled_corpus_files));
for (const std::string& corpus_file : distilled_corpus_files) {
const std::string file_name = std::filesystem::path(corpus_file).filename();
FUZZTEST_CHECK_OK(
RemoteFileRename(corpus_file, (coverage_dir / file_name).c_str()));
}
UpdateCorpus(env, db_paths, callbacks_factory, stop_condition);
}

int ListCrashIds(const Environment& env) {
Expand All @@ -583,9 +615,7 @@ int ListCrashIds(const Environment& env) {
FUZZTEST_CHECK(!env.test_name.empty());
std::vector<std::string> crash_paths;
// TODO: b/406003594 - move the path construction to a library.
const auto crash_dir = std::filesystem::path(env.fuzztest_corpus_database) /
env.fuzztest_binary_identifier / env.test_name /
"crashing";
const auto crash_dir = GetDatabasePaths(env).crashing_dir;
if (RemotePathExists(crash_dir.string())) {
FUZZTEST_CHECK(RemotePathIsDirectory(crash_dir.string()))
<< "Crash dir " << crash_dir << " in the corpus database "
Expand Down
1 change: 1 addition & 0 deletions centipede/environment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ void Environment::UpdateWithTargetConfig(
fuzztest_stats_root = config.stats_root;
fuzztest_workdir_root = config.workdir_root;
fuzztest_only_replay = config.only_replay;
fuzztest_update_corpus_database = config.update_corpus_database;
fuzztest_execution_id = config.execution_id.value_or("");
fuzztest_replay_coverage_inputs = config.replay_coverage_inputs;
fuzztest_time_limit_per_test = config.GetTimeLimitPerTest();
Expand Down
1 change: 1 addition & 0 deletions centipede/environment.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <cstddef>
#include <cstdint>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
Expand Down
1 change: 1 addition & 0 deletions e2e_tests/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ cc_test(
"@abseil-cpp//absl/time",
"@com_google_fuzztest//centipede:weak_sancov_stubs",
"@com_google_fuzztest//common:logging",
"@com_google_fuzztest//common:remote_file",
"@com_google_fuzztest//common:temp_dir",
"@com_google_fuzztest//fuzztest/internal:escaping",
"@com_google_fuzztest//fuzztest/internal:io",
Expand Down
97 changes: 97 additions & 0 deletions e2e_tests/corpus_database_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/string_view.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "./common/logging.h"
#include "./common/remote_file.h"
#include "./common/temp_dir.h"
#include "./e2e_tests/test_binary_util.h"
#include "./fuzztest/internal/escaping.h"
Expand Down Expand Up @@ -394,6 +396,101 @@ TEST_P(UpdateCorpusDatabaseTest, PrintsErrorsWhenBazelTimeoutIsNotEnough) {
"test FuzzTest.FailsWithStackOverflow")));
}

TEST_P(UpdateCorpusDatabaseTest, ReplayDoesNotUpdateDatabaseByDefault) {
const std::string db_path = UpdateCorpusDatabaseAndGetPath();

std::vector<std::string> crash_files;
for (const std::string& path : ListDirectoryRecursively(db_path)) {
std::filesystem::path fs_path(path);
if (fs_path.parent_path().filename() == "crashing" &&
fs_path.parent_path().parent_path().filename() ==
"FuzzTest.FailsInTwoWays") {
crash_files.push_back(path);
}
}
ASSERT_FALSE(crash_files.empty());
const std::string target_crash = crash_files[0];

const auto initial_mtime = RemoteFileGetMTime(target_crash);
ASSERT_TRUE(initial_mtime.ok());
const absl::Time initial_mod_time = *initial_mtime;

absl::SleepFor(absl::Seconds(2));

RunOptions run_options;
run_options.fuzztest_flags = {
{"corpus_database", db_path},
{"replay_corpus", "FuzzTest.FailsInTwoWays"},
{"replay_corpus_for", "10s"},
};

auto [status, std_out, std_err] = RunBinaryMaybeWithCentipede(
GetCorpusDatabaseTestingBinaryPath(), run_options);

const auto final_mtime = RemoteFileGetMTime(target_crash);
ASSERT_TRUE(final_mtime.ok());
EXPECT_EQ(*final_mtime, initial_mod_time);
}

TEST_P(UpdateCorpusDatabaseTest, ReplayUpdatesDatabaseWithFlag) {
const std::string db_path = UpdateCorpusDatabaseAndGetPath();

std::vector<std::string> crash_files;
for (const std::string& path : ListDirectoryRecursively(db_path)) {
std::filesystem::path fs_path(path);
if (fs_path.parent_path().filename() == "crashing" &&
fs_path.parent_path().parent_path().filename() ==
"FuzzTest.FailsInTwoWays") {
crash_files.push_back(path);
}
}
ASSERT_FALSE(crash_files.empty());
const std::string target_crash = crash_files[0];

const auto initial_mtime = RemoteFileGetMTime(target_crash);
ASSERT_TRUE(initial_mtime.ok());
const absl::Time initial_mod_time = *initial_mtime;

absl::SleepFor(absl::Seconds(2));

RunOptions run_options;
run_options.fuzztest_flags = {
{"corpus_database", db_path},
{"replay_corpus", "FuzzTest.FailsInTwoWays"},
{"replay_corpus_for", "10s"},
{"update_corpus_database", "true"},
};

auto [status, std_out, std_err] = RunBinaryMaybeWithCentipede(
GetCorpusDatabaseTestingBinaryPath(), run_options);

const auto final_mtime = RemoteFileGetMTime(target_crash);
ASSERT_TRUE(final_mtime.ok());
EXPECT_GT(*final_mtime, initial_mod_time)
<< "target_crash: " << target_crash << "\nstd_err:\n"
<< std_err << "\nstd_out:\n"
<< std_out;
}

TEST_P(UpdateCorpusDatabaseTest, FuzzingDoesNotUpdateDatabaseWithFlag) {
TempDir corpus_database;

RunOptions run_options;
run_options.fuzztest_flags = {
{"corpus_database", corpus_database.path()},
{"fuzz_for", "10s"},
{"update_corpus_database", "false"},
};
run_options.timeout = absl::Seconds(10);
auto [status, std_out, std_err] = RunBinaryMaybeWithCentipede(
GetCorpusDatabaseTestingBinaryPath(), run_options);

// The database should not be updated, so fuzzing_time should not exist.
const absl::StatusOr<std::string> fuzzing_time_file =
FindFile(corpus_database.path().c_str(), "fuzzing_time");
EXPECT_FALSE(fuzzing_time_file.ok()) << *fuzzing_time_file;
}

INSTANTIATE_TEST_SUITE_P(
UpdateCorpusDatabaseTestWithExecutionModel, UpdateCorpusDatabaseTest,
testing::ValuesIn({
Expand Down
19 changes: 19 additions & 0 deletions fuzztest/init_fuzztest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ FUZZTEST_DEFINE_FLAG(
". In the latter case, each FUZZ_TEST will run for at most (1/N)th of the "
"time budget.");

FUZZTEST_DEFINE_FLAG(
std::optional<bool>, update_corpus_database, std::nullopt,
"If true, update the corpus database (both crashes and coverage). "
"If false, do not update it. "
"If unspecified, it defaults to true in fuzzing mode and false in replay "
"mode.");

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 "
Expand Down Expand Up @@ -328,6 +335,13 @@ std::optional<absl::Duration> GetReplayCorpusTime() {
return replay_corpus_time_limit;
}

bool GetDefaultUpdateCorpusDatabase() {
const std::string test_to_fuzz = absl::GetFlag(FUZZTEST_FLAG(fuzz));
const bool is_fuzzing =
test_to_fuzz != kUnspecified || GetFuzzingTime().has_value();
return is_fuzzing;
}

internal::Configuration CreateConfigurationsFromFlags(
absl::string_view binary_identifier) {
const bool reproduce_findings_as_separate_tests =
Expand All @@ -339,6 +353,10 @@ internal::Configuration CreateConfigurationsFromFlags(
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 bool update_corpus_database =
absl::GetFlag(FUZZTEST_FLAG(update_corpus_database))
.value_or(GetDefaultUpdateCorpusDatabase());

const absl::Duration time_limit =
override_fuzz_test.has_value()
? absl::GetFlag(FUZZTEST_FLAG(internal_override_total_time_limit))
Expand Down Expand Up @@ -385,6 +403,7 @@ internal::Configuration CreateConfigurationsFromFlags(
replay_coverage_inputs,
/*only_replay=*/
replay_corpus_time_limit.has_value(),
update_corpus_database,
/*replay_in_single_process=*/false,
absl::GetFlag(FUZZTEST_FLAG(execution_id)),
absl::GetFlag(FUZZTEST_FLAG(print_subprocess_log)),
Expand Down
3 changes: 2 additions & 1 deletion fuzztest/internal/centipede_adaptor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -967,7 +967,8 @@ bool CentipedeFuzzerAdaptor::Run(int* argc, char*** argv, RunMode mode,
runtime_.SetShouldTerminateOnNonFatalFailure(false);
std::unique_ptr<TempDir> workdir;
if (configuration.corpus_database.empty() ||
(mode == RunMode::kUnitTest && configuration.workdir_root.empty())) {
(!configuration.update_corpus_database &&
configuration.workdir_root.empty())) {
workdir = std::make_unique<TempDir>("fuzztest_workdir");
}
const std::string workdir_path = workdir ? workdir->path() : "";
Expand Down
Loading
Loading